summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEd Tanous <ed@tanous.net>2024-04-14 19:57:09 +0300
committerEd Tanous <ed@tanous.net>2024-04-23 17:58:29 +0300
commitc056aa7aa2438d16b1a3f1db20e6aac2694ca455 (patch)
tree7328ae94e1c35298b27d542480d42dcff36d7203
parent003301a24bad4cfe066bb1f5720243a3f7c45742 (diff)
downloadbmcweb-c056aa7aa2438d16b1a3f1db20e6aac2694ca455.tar.xz
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>
-rw-r--r--include/security_headers.hpp165
-rw-r--r--test/http/http2_connection_test.cpp35
-rw-r--r--test/http/http_connection_test.cpp8
3 files changed, 93 insertions, 115 deletions
diff --git a/include/security_headers.hpp b/include/security_headers.hpp
index 9af494d21b..a9c3fc419a 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 81c78a9a60..34dc7cd20e 100644
--- a/test/http/http2_connection_test.cpp
+++ b/test/http/http2_connection_test.cpp
@@ -136,8 +136,8 @@ TEST(http_connection, RequestPropogates)
// 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 @@ TEST(http_connection, RequestPropogates)
"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 @@ TEST(http_connection, RequestPropogates)
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 4dda70ecf8..caf50c8a62 100644
--- a/test/http/http_connection_test.cpp
+++ b/test/http/http_connection_test.cpp
@@ -84,17 +84,9 @@ TEST(http_connection, RequestPropogates)
"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);