Implement a Content-Security-Policy TODO

This TODO has been in bmcweb for a very long time.  Implement it.

W3 sets rules for what security policies apply to which content
types[1].  Reading through this, essentially CSP should only apply to
HTML files.

Tested: Unit tests pass.  Webui loads properly.  Chrome network window
Shows headers show up as expected.

[1] https://www.w3.org/TR/CSP2/#which-policy-applies

Change-Id: I5467d0373832668763c72a66da2a8872e07bfb58
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/include/security_headers.hpp b/include/security_headers.hpp
index 9af494d..a9c3fc4 100644
--- a/include/security_headers.hpp
+++ b/include/security_headers.hpp
@@ -8,102 +8,101 @@
 inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]],
                                crow::Response& res)
 {
-    /*
-     TODO(ed) these should really check content types.  for example,
-     X-Content-Type-Options header doesn't make sense when retrieving a JSON or
-     javascript file.  It doesn't hurt anything, it's just ugly.
-     */
     using bf = boost::beast::http::field;
 
     // Recommendations from https://owasp.org/www-project-secure-headers/
     // https://owasp.org/www-project-secure-headers/ci/headers_add.json
     res.addHeader(bf::strict_transport_security, "max-age=31536000; "
                                                  "includeSubdomains");
-    res.addHeader(bf::x_frame_options, "DENY");
 
     res.addHeader(bf::pragma, "no-cache");
     res.addHeader(bf::cache_control, "no-store, max-age=0");
 
     res.addHeader("X-Content-Type-Options", "nosniff");
 
-    res.addHeader("Referrer-Policy", "no-referrer");
-    res.addHeader("Permissions-Policy", "accelerometer=(),"
-                                        "ambient-light-sensor=(),"
-                                        "autoplay=(),"
-                                        "battery=(),"
-                                        "camera=(),"
-                                        "display-capture=(),"
-                                        "document-domain=(),"
-                                        "encrypted-media=(),"
-                                        "fullscreen=(),"
-                                        "gamepad=(),"
-                                        "geolocation=(),"
-                                        "gyroscope=(),"
-                                        "layout-animations=(self),"
-                                        "legacy-image-formats=(self),"
-                                        "magnetometer=(),"
-                                        "microphone=(),"
-                                        "midi=(),"
-                                        "oversized-images=(self),"
-                                        "payment=(),"
-                                        "picture-in-picture=(),"
-                                        "publickey-credentials-get=(),"
-                                        "speaker-selection=(),"
-                                        "sync-xhr=(self),"
-                                        "unoptimized-images=(self),"
-                                        "unsized-media=(self),"
-                                        "usb=(),"
-                                        "screen-wak-lock=(),"
-                                        "web-share=(),"
-                                        "xr-spatial-tracking=()");
-    res.addHeader("X-Permitted-Cross-Domain-Policies", "none");
-    res.addHeader("Cross-Origin-Embedder-Policy", "require-corp");
-    res.addHeader("Cross-Origin-Opener-Policy", "same-origin");
-    res.addHeader("Cross-Origin-Resource-Policy", "same-origin");
-    if (bmcwebInsecureDisableXssPrevention == 0)
+    std::string_view contentType = res.getHeaderValue("Content-Type");
+    if (contentType.starts_with("text/html"))
     {
-        res.addHeader("Content-Security-Policy", "default-src 'none'; "
-                                                 "img-src 'self' data:; "
-                                                 "font-src 'self'; "
-                                                 "style-src 'self'; "
-                                                 "script-src 'self'; "
-                                                 "connect-src 'self' wss:; "
-                                                 "form-action 'none'; "
-                                                 "frame-ancestors 'none'; "
-                                                 "object-src 'none'; "
-                                                 "base-uri 'none' ");
-        // The KVM currently needs to load images from base64 encoded
-        // strings. img-src 'self' data: is used to allow that.
-        // https://stackoverflow.com/questions/18447970/content-security-polic
-        // y-data-not-working-for-base64-images-in-chrome-28
-    }
-    else
-    {
-        // If XSS is disabled, we need to allow loading from addresses other
-        // than self, as the BMC will be hosted elsewhere.
-        res.addHeader("Content-Security-Policy", "default-src 'none'; "
-                                                 "img-src * data:; "
-                                                 "font-src *; "
-                                                 "style-src *; "
-                                                 "script-src *; "
-                                                 "connect-src *; "
-                                                 "form-action *; "
-                                                 "frame-ancestors *; "
-                                                 "object-src *; "
-                                                 "base-uri *");
+        res.addHeader(bf::x_frame_options, "DENY");
+        res.addHeader("Referrer-Policy", "no-referrer");
+        res.addHeader("Permissions-Policy", "accelerometer=(),"
+                                            "ambient-light-sensor=(),"
+                                            "autoplay=(),"
+                                            "battery=(),"
+                                            "camera=(),"
+                                            "display-capture=(),"
+                                            "document-domain=(),"
+                                            "encrypted-media=(),"
+                                            "fullscreen=(),"
+                                            "gamepad=(),"
+                                            "geolocation=(),"
+                                            "gyroscope=(),"
+                                            "layout-animations=(self),"
+                                            "legacy-image-formats=(self),"
+                                            "magnetometer=(),"
+                                            "microphone=(),"
+                                            "midi=(),"
+                                            "oversized-images=(self),"
+                                            "payment=(),"
+                                            "picture-in-picture=(),"
+                                            "publickey-credentials-get=(),"
+                                            "speaker-selection=(),"
+                                            "sync-xhr=(self),"
+                                            "unoptimized-images=(self),"
+                                            "unsized-media=(self),"
+                                            "usb=(),"
+                                            "screen-wak-lock=(),"
+                                            "web-share=(),"
+                                            "xr-spatial-tracking=()");
+        res.addHeader("X-Permitted-Cross-Domain-Policies", "none");
+        res.addHeader("Cross-Origin-Embedder-Policy", "require-corp");
+        res.addHeader("Cross-Origin-Opener-Policy", "same-origin");
+        res.addHeader("Cross-Origin-Resource-Policy", "same-origin");
+        if (bmcwebInsecureDisableXssPrevention == 0)
+        {
+            res.addHeader("Content-Security-Policy", "default-src 'none'; "
+                                                     "img-src 'self' data:; "
+                                                     "font-src 'self'; "
+                                                     "style-src 'self'; "
+                                                     "script-src 'self'; "
+                                                     "connect-src 'self' wss:; "
+                                                     "form-action 'none'; "
+                                                     "frame-ancestors 'none'; "
+                                                     "object-src 'none'; "
+                                                     "base-uri 'none' ");
+            // The KVM currently needs to load images from base64 encoded
+            // strings. img-src 'self' data: is used to allow that.
+            // https://stackoverflow.com/questions/18447970/content-security-polic
+            // y-data-not-working-for-base64-images-in-chrome-28
+        }
+        else
+        {
+            // If XSS is disabled, we need to allow loading from addresses
+            // other than self, as the BMC will be hosted elsewhere.
+            res.addHeader("Content-Security-Policy", "default-src 'none'; "
+                                                     "img-src * data:; "
+                                                     "font-src *; "
+                                                     "style-src *; "
+                                                     "script-src *; "
+                                                     "connect-src *; "
+                                                     "form-action *; "
+                                                     "frame-ancestors *; "
+                                                     "object-src *; "
+                                                     "base-uri *");
 
-        std::string_view origin = req.getHeaderValue("Origin");
-        res.addHeader(bf::access_control_allow_origin, origin);
-        res.addHeader(bf::access_control_allow_methods, "GET, "
-                                                        "POST, "
-                                                        "PUT, "
-                                                        "PATCH, "
-                                                        "DELETE");
-        res.addHeader(bf::access_control_allow_credentials, "true");
-        res.addHeader(bf::access_control_allow_headers, "Origin, "
-                                                        "Content-Type, "
-                                                        "Accept, "
-                                                        "Cookie, "
-                                                        "X-XSRF-TOKEN");
+            std::string_view origin = req.getHeaderValue("Origin");
+            res.addHeader(bf::access_control_allow_origin, origin);
+            res.addHeader(bf::access_control_allow_methods, "GET, "
+                                                            "POST, "
+                                                            "PUT, "
+                                                            "PATCH, "
+                                                            "DELETE");
+            res.addHeader(bf::access_control_allow_credentials, "true");
+            res.addHeader(bf::access_control_allow_headers, "Origin, "
+                                                            "Content-Type, "
+                                                            "Accept, "
+                                                            "Cookie, "
+                                                            "X-XSRF-TOKEN");
+        }
     }
 }
diff --git a/test/http/http2_connection_test.cpp b/test/http/http2_connection_test.cpp
index 81c78a9..34dc7cd 100644
--- a/test/http/http2_connection_test.cpp
+++ b/test/http/http2_connection_test.cpp
@@ -136,8 +136,8 @@
         // Settings ACK from server to client
         "\x00\x00\x00\x04\x01\x00\x00\x00\x00"
 
-        // Start Headers frame stream 1, size 0x034b
-        "\x00\x03\x4b\x01\x04\x00\x00\x00\x01"sv;
+        // Start Headers frame stream 1, size 0x005f
+        "\x00\x00\x5f\x01\x04\x00\x00\x00\x01"sv;
 
     std::string_view expectedPostfix =
         // Data Frame, Length 12, Stream 1, End Stream flag set
@@ -146,7 +146,7 @@
         "StringOutput"sv;
 
     std::string_view outStr;
-    constexpr size_t headerSize = 0x34b;
+    constexpr size_t headerSize = 0x05f;
 
     // Run until we receive the expected amount of data
     while (outStr.size() <
@@ -164,27 +164,14 @@
     unpackHeaders(outStr.substr(0, headerSize), headers);
     outStr.remove_prefix(headerSize);
 
-    EXPECT_THAT(
-        headers,
-        UnorderedElementsAre(
-            Pair(":status", "200"), Pair("content-length", "12"),
-            Pair("strict-transport-security",
-                 "max-age=31536000; includeSubdomains"),
-            Pair("x-frame-options", "DENY"), Pair("pragma", "no-cache"),
-            Pair("cache-control", "no-store, max-age=0"),
-            Pair("x-content-type-options", "nosniff"),
-            Pair("referrer-policy", "no-referrer"),
-            Pair(
-                "permissions-policy",
-                "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wak-lock=(),web-share=(),xr-spatial-tracking=()"),
-            Pair("x-permitted-cross-domain-policies", "none"),
-            Pair("cross-origin-embedder-policy", "require-corp"),
-            Pair("cross-origin-opener-policy", "same-origin"),
-            Pair("cross-origin-resource-policy", "same-origin"),
-            Pair(
-                "content-security-policy",
-                "default-src 'none'; img-src 'self' data:; font-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self' wss:; form-action 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'none'"),
-            Pair("date", "TestTime")));
+    EXPECT_THAT(headers,
+                UnorderedElementsAre(
+                    Pair(":status", "200"), Pair("content-length", "12"),
+                    Pair("strict-transport-security",
+                         "max-age=31536000; includeSubdomains"),
+                    Pair("cache-control", "no-store, max-age=0"),
+                    Pair("x-content-type-options", "nosniff"),
+                    Pair("pragma", "no-cache"), Pair("date", "TestTime")));
 
     EXPECT_EQ(outStr, expectedPostfix);
 }
diff --git a/test/http/http_connection_test.cpp b/test/http/http_connection_test.cpp
index 4dda70e..caf50c8 100644
--- a/test/http/http_connection_test.cpp
+++ b/test/http/http_connection_test.cpp
@@ -84,17 +84,9 @@
         "HTTP/1.1 200 OK\r\n"
         "Connection: close\r\n"
         "Strict-Transport-Security: max-age=31536000; includeSubdomains\r\n"
-        "X-Frame-Options: DENY\r\n"
         "Pragma: no-cache\r\n"
         "Cache-Control: no-store, max-age=0\r\n"
         "X-Content-Type-Options: nosniff\r\n"
-        "Referrer-Policy: no-referrer\r\n"
-        "Permissions-Policy: accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wak-lock=(),web-share=(),xr-spatial-tracking=()\r\n"
-        "X-Permitted-Cross-Domain-Policies: none\r\n"
-        "Cross-Origin-Embedder-Policy: require-corp\r\n"
-        "Cross-Origin-Opener-Policy: same-origin\r\n"
-        "Cross-Origin-Resource-Policy: same-origin\r\n"
-        "Content-Security-Policy: default-src 'none'; img-src 'self' data:; font-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self' wss:; form-action 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'none'\r\n"
         "Date: TestTime\r\n"
         "Content-Length: 0\r\n\r\n";
     EXPECT_EQ(outStr, expected);