From 5d362cd07314f76094458e017ddf46b9214b9463 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Sun, 21 Dec 2025 20:59:55 +0100 Subject: [PATCH 01/26] feat: introduce helmet middleware and unit tests. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 76 ++++++++++++++++++++ src/server/helmet.cpp | 81 ++++++++++++++++++++++ test/unit/server/helmet.cpp | 33 +++++++++ 3 files changed, 190 insertions(+) create mode 100644 include/boost/http_proto/server/helmet.hpp create mode 100644 src/server/helmet.cpp create mode 100644 test/unit/server/helmet.cpp diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp new file mode 100644 index 00000000..4dec97f6 --- /dev/null +++ b/include/boost/http_proto/server/helmet.hpp @@ -0,0 +1,76 @@ +// +// Copyright (c) 2025 Amlal El Mahrouss (amlal at nekernel dot org) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +#ifndef BOOST_HTTP_PROTO_SERVER_HELMET_HPP +#define BOOST_HTTP_PROTO_SERVER_HELMET_HPP + +#include +#include +#include + +namespace boost { +namespace http_proto { + +/// @brief Helmet options. +struct options +{ + using pair_type = std::pair>; + using map_type = std::vector; + + /// @brief {key, enabled} + /// @note i.e {bad-header, ""} <-- disabled + map_type headers = { + {"Content-Security-Policy", {"default-src 'self'", "base-uri 'self'", "font-src 'self' https: data:", "form-action 'self'", "frame-ancestors 'self'", + "img-src 'self' data:", "object-src 'none'", "script-src 'self'", "script-src-attr 'none'", "style-src 'self' https: 'unsafe-inline'", "upgrade-insecure-requests"}}, + {"Cross-Origin-Embedder-Policy", {"require-corp"}}, + {"Cross-Origin-Opener-Policy", {"same-origin"}}, + {"Cross-Origin-Resource-Policy", {"same-origin"}}, + {"X-DNS-Prefetch-Control", {"off"}}, + {"Expect-CT", {"max-age=86400, enforce"}}, + {"X-Frame-Options", {"SAMEORIGIN"}}, + {"X-Powered-By", {""}}, // Remove this header + {"Strict-Transport-Security", {"max-age=15552000", "includeSubDomains"}}, + {"X-Download-Options", {"noopen"}}, + {"X-Content-Type-Options", {"nosniff"}}, + {"Origin-Agent-Cluster", {"?1"}}, + {"X-Permitted-Cross-Domain-Policies", {"none"}}, + {"Referrer-Policy", {"no-referrer"}}, + {"X-XSS-Protection", {"0"}} // Disabled as modern browsers have better protections + }; +}; + +/// @brief Security middleware inspired by express.js concept of helmets. +class helmet +{ + struct impl; + std::unique_ptr impl_; + +public: + /// @brief Builds an helmet and compute its options for caching purposes. + BOOST_HTTP_PROTO_DECL + explicit helmet( + options options = {}); + + /// @brief Iterates over cachedHeaders and apply its rules to the response params. + /// @param p route parameter argument + /// @return route_result an error_code signaling the route's status. + BOOST_HTTP_PROTO_DECL + route_result + operator()(route_params& p) const; + +}; + +#if !defined(BOOST_HTTP_PROTO_HELMET_IMPL) +struct helmet::impl {}; +#endif +} + +} + +#endif diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp new file mode 100644 index 00000000..164a0a53 --- /dev/null +++ b/src/server/helmet.cpp @@ -0,0 +1,81 @@ +// +// Copyright (c) 2025 Amlal El Mahrouss (amlal at nekernel dot org) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +#define BOOST_HTTP_PROTO_HELMET_IMPL 1 + +#include +#include + +namespace boost { +namespace http_proto { + +namespace detail { + + auto setHeaderValues(const std::vector& fields_value) -> std::string + { + if (fields_value.empty()) + detail::throw_invalid_argument(); + + std::string cached_fields; + bool is_first = true; + + for (const auto& field : fields_value) + { + cached_fields += field; + if (!is_first) cached_fields += "; "; + is_first = false; + } + + return cached_fields; + } + +} + +/// @brief private implementation details for the `helmet` middleware. +/// @internal +struct helmet::impl +{ + using pair_type = std::pair; + using vector_type = std::vector; + + options options_; + vector_type cached_headers_{}; +}; + +helmet:: +helmet( + options options) +{ + impl_ = std::make_unique(); + impl_->options_ = std::move(options); + + std::for_each(impl_->options_.headers.begin(), impl_->options_.headers.end(), + [this] (const auto& hdr) { + this->impl_->cached_headers_.push_back(std::make_pair(hdr.first, detail::setHeaderValues(hdr.second))); + }); +} + +route_result +helmet:: +operator()( + route_params& p) const +{ + std::for_each(impl_->cached_headers_.begin(), impl_->cached_headers_.end(), + [&p] (const auto& hdr) { + if (hdr.first.empty()) + detail::throw_logic_error(); + + p.res.set(hdr.first, hdr.second); + }); + + return route::next; +} +} + +} diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp new file mode 100644 index 00000000..f6399148 --- /dev/null +++ b/test/unit/server/helmet.cpp @@ -0,0 +1,33 @@ +// +// Copyright (c) 2025 Amlal El Mahrouss (amlal at nekernel dot org) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +#include +#include "test_suite.hpp" + +namespace boost { +namespace http_proto { + +struct helmet_test +{ + void + run() + { + helmet helmet{}; + route_params p; + auto ec = helmet(p); + BOOST_TEST(ec == route::next); + } +}; + +TEST_SUITE( + helmet_test, + "boost.http_proto.server.helmet"); +} + +} From 0d234650989351c5da3b776a692a002f21d93062 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Tue, 23 Dec 2025 19:43:21 +0100 Subject: [PATCH 02/26] draft: helmet: design fixes on the option structure. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 49 ++++++++-------------- src/server/helmet.cpp | 13 +++--- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 4dec97f6..6a177f41 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -17,58 +17,43 @@ namespace boost { namespace http_proto { -/// @brief Helmet options. -struct options +struct helmet_options { using pair_type = std::pair>; using map_type = std::vector; - /// @brief {key, enabled} - /// @note i.e {bad-header, ""} <-- disabled - map_type headers = { - {"Content-Security-Policy", {"default-src 'self'", "base-uri 'self'", "font-src 'self' https: data:", "form-action 'self'", "frame-ancestors 'self'", - "img-src 'self' data:", "object-src 'none'", "script-src 'self'", "script-src-attr 'none'", "style-src 'self' https: 'unsafe-inline'", "upgrade-insecure-requests"}}, - {"Cross-Origin-Embedder-Policy", {"require-corp"}}, - {"Cross-Origin-Opener-Policy", {"same-origin"}}, - {"Cross-Origin-Resource-Policy", {"same-origin"}}, - {"X-DNS-Prefetch-Control", {"off"}}, - {"Expect-CT", {"max-age=86400, enforce"}}, - {"X-Frame-Options", {"SAMEORIGIN"}}, - {"X-Powered-By", {""}}, // Remove this header - {"Strict-Transport-Security", {"max-age=15552000", "includeSubDomains"}}, - {"X-Download-Options", {"noopen"}}, - {"X-Content-Type-Options", {"nosniff"}}, - {"Origin-Agent-Cluster", {"?1"}}, - {"X-Permitted-Cross-Domain-Policies", {"none"}}, - {"Referrer-Policy", {"no-referrer"}}, - {"X-XSS-Protection", {"0"}} // Disabled as modern browsers have better protections - }; + map_type headers; }; -/// @brief Security middleware inspired by express.js concept of helmets. +/** + @brief Security middleware inspired by express.js concept of helmets. +*/ class helmet { struct impl; std::unique_ptr impl_; public: - /// @brief Builds an helmet and compute its options for caching purposes. + /** + @brief Builds an helmet and compute its options for caching purposes. + */ BOOST_HTTP_PROTO_DECL explicit helmet( - options options = {}); + helmet_options options = {}); - /// @brief Iterates over cachedHeaders and apply its rules to the response params. - /// @param p route parameter argument - /// @return route_result an error_code signaling the route's status. + BOOST_HTTP_PROTO_DECL + ~helmet(); + + /** + @brief Iterates over cachedHeaders and apply its rules to the response params. + @param p route parameter argument + @return route_result an error_code signaling the route's status. + */ BOOST_HTTP_PROTO_DECL route_result operator()(route_params& p) const; }; - -#if !defined(BOOST_HTTP_PROTO_HELMET_IMPL) -struct helmet::impl {}; -#endif } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 164a0a53..46c07dda 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -37,23 +37,24 @@ namespace detail { } -/// @brief private implementation details for the `helmet` middleware. -/// @internal +/** + @brief private implementation details for the `helmet` middleware. +*/ struct helmet::impl { using pair_type = std::pair; using vector_type = std::vector; - options options_; + helmet_options options_; vector_type cached_headers_{}; }; helmet:: helmet( - options options) + helmet_options helmet_options) { impl_ = std::make_unique(); - impl_->options_ = std::move(options); + impl_->options_ = std::move(helmet_options); std::for_each(impl_->options_.headers.begin(), impl_->options_.headers.end(), [this] (const auto& hdr) { @@ -61,6 +62,8 @@ helmet( }); } +helmet::~helmet() {} + route_result helmet:: operator()( From cc1a3ad000d1eff4b18608406def1134ae5ed2a2 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Tue, 23 Dec 2025 20:03:51 +0100 Subject: [PATCH 03/26] fix: helmet.cpp: use default destructor for `helmet`. Signed-off-by: Amlal El Mahrouss --- src/server/helmet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 46c07dda..f897c1bd 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -62,7 +62,7 @@ helmet( }); } -helmet::~helmet() {} +helmet::~helmet() = default; route_result helmet:: From 03d0cd5e8d457ba52b9be72fb7fadedddc8855f3 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 09:46:18 +0100 Subject: [PATCH 04/26] feat: helmet.hpp: API redesign. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 48 ++++++++++++++++++++++ src/server/helmet.cpp | 5 +-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 6a177f41..d392b536 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -23,6 +23,25 @@ struct helmet_options using map_type = std::vector; map_type headers; + + /** + @brief Sets the value at a specific location. + @param helmet_hdr the indexable header. + @return helmet_options instance. + */ + helmet_options& set(const pair_type& helmet_hdr) + { + auto it_hdr = std::find(headers.begin(), headers.end(), helmet_hdr); + + if (it_hdr != headers.end()) + { + *it_hdr = helmet_hdr; + return *this; + } + + headers.push_back(helmet_hdr); + return *this; + } }; /** @@ -54,6 +73,35 @@ class helmet operator()(route_params& p) const; }; + + +enum class helmet_download_type { noopen, disabled }; + +/** + @brief Enable the X-Download-Options header. +*/ +inline std::pair> x_download_options(const helmet_download_type& type) +{ + if (type == helmet_download_type::noopen) + { + return {"X-Download-Options", {"noopen"}}; + } + + return {}; +} + +enum class helmet_origin_type { deny, sameorigin }; + +/** @brief Enable the X-Frame-Origin header */ +inline std::pair> x_frame_origin(const helmet_origin_type& origin) +{ + if (origin == helmet_origin_type::sameorigin) + { + return {"X-Frame-Options", {"SAMEORIGIN"}}; + } + + return {"X-Frame-Options", {"DENY"}}; +} } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index f897c1bd..c704af39 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -20,7 +20,7 @@ namespace detail { auto setHeaderValues(const std::vector& fields_value) -> std::string { if (fields_value.empty()) - detail::throw_invalid_argument(); + return {}; std::string cached_fields; bool is_first = true; @@ -71,9 +71,6 @@ operator()( { std::for_each(impl_->cached_headers_.begin(), impl_->cached_headers_.end(), [&p] (const auto& hdr) { - if (hdr.first.empty()) - detail::throw_logic_error(); - p.res.set(hdr.first, hdr.second); }); From 386eef53bf44ab340e66bfb357b3bcfbca462489 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 09:49:24 +0100 Subject: [PATCH 05/26] fix: helmet.hpp: remove extra space after helmet structure. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index d392b536..7a200866 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -74,7 +74,6 @@ class helmet }; - enum class helmet_download_type { noopen, disabled }; /** From beff5491f855fc0af2ab5717f75f2caac46755dc Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 10:23:56 +0100 Subject: [PATCH 06/26] feat: helmet: public API improvements and add two unit tests. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 25 +++++++--------- src/server/helmet.cpp | 35 ++++++++++++++++++++-- test/unit/server/helmet.cpp | 18 ++++++++--- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 7a200866..f446a2c4 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -29,19 +29,10 @@ struct helmet_options @param helmet_hdr the indexable header. @return helmet_options instance. */ - helmet_options& set(const pair_type& helmet_hdr) - { - auto it_hdr = std::find(headers.begin(), headers.end(), helmet_hdr); - - if (it_hdr != headers.end()) - { - *it_hdr = helmet_hdr; - return *this; - } - - headers.push_back(helmet_hdr); - return *this; - } + helmet_options& set(const pair_type& helmet_hdr); + + helmet_options(); + ~helmet_options(); }; /** @@ -75,6 +66,7 @@ class helmet }; enum class helmet_download_type { noopen, disabled }; +enum class helmet_origin_type { deny, sameorigin }; /** @brief Enable the X-Download-Options header. @@ -89,8 +81,6 @@ inline std::pair> x_download_options(const return {}; } -enum class helmet_origin_type { deny, sameorigin }; - /** @brief Enable the X-Frame-Origin header */ inline std::pair> x_frame_origin(const helmet_origin_type& origin) { @@ -101,6 +91,11 @@ inline std::pair> x_frame_origin(const hel return {"X-Frame-Options", {"DENY"}}; } + +inline std::pair> x_xss_protection() +{ + return {"X-Frame-Options", {""}}; +} } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index c704af39..3107a6b1 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -37,6 +37,32 @@ namespace detail { } +helmet_options::helmet_options() +{ + this->set(x_download_options(helmet_download_type::noopen)); + this->set(x_frame_origin(helmet_origin_type::deny)); + this->set(x_xss_protection()); +} + +helmet_options::~helmet_options() = default; + +helmet_options& helmet_options::set(const pair_type& helmet_hdr) +{ + auto it_hdr = std::find_if(headers.begin(), headers.end(), [&helmet_hdr](const pair_type& pair) { + return pair.first == helmet_hdr.first; + }); + + if (it_hdr != headers.end()) + { + *it_hdr = helmet_hdr; + + return *this; + } + + headers.emplace_back(helmet_hdr); + return *this; +} + /** @brief private implementation details for the `helmet` middleware. */ @@ -57,8 +83,10 @@ helmet( impl_->options_ = std::move(helmet_options); std::for_each(impl_->options_.headers.begin(), impl_->options_.headers.end(), - [this] (const auto& hdr) { - this->impl_->cached_headers_.push_back(std::make_pair(hdr.first, detail::setHeaderValues(hdr.second))); + [this] (const helmet_options::pair_type& hdr) + { + this->impl_->cached_headers_.emplace_back(hdr.first, + detail::setHeaderValues(hdr.second)); }); } @@ -70,7 +98,8 @@ operator()( route_params& p) const { std::for_each(impl_->cached_headers_.begin(), impl_->cached_headers_.end(), - [&p] (const auto& hdr) { + [&p] (const impl::pair_type& hdr) + { p.res.set(hdr.first, hdr.second); }); diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp index f6399148..ce3d07ce 100644 --- a/test/unit/server/helmet.cpp +++ b/test/unit/server/helmet.cpp @@ -18,10 +18,20 @@ struct helmet_test void run() { - helmet helmet{}; - route_params p; - auto ec = helmet(p); - BOOST_TEST(ec == route::next); + // X-Download-Options + { + helmet_options opt; + + opt.set(x_download_options(helmet_download_type::noopen)); + + helmet helmet{opt}; + route_params p; + + p.req.append("X-Download-Options", "noopen"); + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + } } }; From 32fde1d599a8ec725ee4f060fdbc7d520f5b272b Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 11:13:39 +0100 Subject: [PATCH 07/26] feat: `sp` namespace for `Security Policy` functions. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 33 ++++++++++++++++++++++ src/server/helmet.cpp | 5 ++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index f446a2c4..53945ff5 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -96,6 +96,39 @@ inline std::pair> x_xss_protection() { return {"X-Frame-Options", {""}}; } + +inline std::pair> x_content_type_options() +{ + return {"X-Content-Type-Options", {"nosniff"}}; +} + +inline std::pair> content_security_policy(const std::vector& allow_list) +{ + return {"Content-Security-Policy", allow_list}; +} + +/** @brief Security Policy namespace for Content-Security-Policy */ +namespace sp +{ + enum class allow_type { self, none, unsafe_inline }; + + inline void append(std::vector& allow_list, const core::string_view& allow, const allow_type& type) + { + if (std::find(allow_list.cbegin(), allow_list.cend(), allow) == allow_list.cend()) + { + std::string final = allow; + + if (type == allow_type::self) + final += ": 'self'"; + else if (type == allow_type::unsafe_inline) + final += ": 'unsafe-inline'"; + else + final += ": 'none'"; + + allow_list.emplace_back(final); + } + } +} } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 3107a6b1..28662d1d 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -23,13 +23,11 @@ namespace detail { return {}; std::string cached_fields; - bool is_first = true; for (const auto& field : fields_value) { cached_fields += field; - if (!is_first) cached_fields += "; "; - is_first = false; + cached_fields += "; "; } return cached_fields; @@ -42,6 +40,7 @@ helmet_options::helmet_options() this->set(x_download_options(helmet_download_type::noopen)); this->set(x_frame_origin(helmet_origin_type::deny)); this->set(x_xss_protection()); + this->set(x_content_type_options()); } helmet_options::~helmet_options() = default; From c99db31b81957fa922007cb032890e68a1a71617 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 11:56:48 +0100 Subject: [PATCH 08/26] fix: sp API and unit test fixes. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 36 +++++++++++++++------- src/server/helmet.cpp | 6 +++- test/unit/server/helmet.cpp | 2 +- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 53945ff5..fb83144b 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -68,10 +68,12 @@ class helmet enum class helmet_download_type { noopen, disabled }; enum class helmet_origin_type { deny, sameorigin }; +using option_pair = std::pair>; + /** @brief Enable the X-Download-Options header. */ -inline std::pair> x_download_options(const helmet_download_type& type) +inline option_pair x_download_options(const helmet_download_type& type) { if (type == helmet_download_type::noopen) { @@ -82,27 +84,31 @@ inline std::pair> x_download_options(const } /** @brief Enable the X-Frame-Origin header */ -inline std::pair> x_frame_origin(const helmet_origin_type& origin) +inline option_pair x_frame_origin(const helmet_origin_type& origin) { if (origin == helmet_origin_type::sameorigin) { return {"X-Frame-Options", {"SAMEORIGIN"}}; } + else if (origin == helmet_origin_type::deny) + { + return {"X-Frame-Options", {"DENY"}}; + } - return {"X-Frame-Options", {"DENY"}}; + return {}; } -inline std::pair> x_xss_protection() +inline option_pair x_xss_protection() { - return {"X-Frame-Options", {""}}; + return {"X-XSS-Protection", {"0"}}; } -inline std::pair> x_content_type_options() +inline option_pair x_content_type_options() { return {"X-Content-Type-Options", {"nosniff"}}; } -inline std::pair> content_security_policy(const std::vector& allow_list) +inline option_pair content_security_policy(const std::vector& allow_list) { return {"Content-Security-Policy", allow_list}; } @@ -112,22 +118,30 @@ namespace sp { enum class allow_type { self, none, unsafe_inline }; - inline void append(std::vector& allow_list, const core::string_view& allow, const allow_type& type) + inline void push_back(std::vector& allow_list, const core::string_view& allow, const allow_type& type) { if (std::find(allow_list.cbegin(), allow_list.cend(), allow) == allow_list.cend()) { std::string final = allow; if (type == allow_type::self) - final += ": 'self'"; + final += " 'self'"; else if (type == allow_type::unsafe_inline) - final += ": 'unsafe-inline'"; + final += " 'unsafe-inline'"; else - final += ": 'none'"; + final += " 'none'"; allow_list.emplace_back(final); } } + + inline void remove(std::vector& allow_list, const core::string_view& allow) + { + if (auto it = std::find(allow_list.cbegin(), allow_list.cend(), allow); it != allow_list.cend()) + { + allow_list.erase(it); + } + } } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 28662d1d..85b4a69b 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -24,10 +24,14 @@ namespace detail { std::string cached_fields; + std::size_t index{}; + for (const auto& field : fields_value) { cached_fields += field; - cached_fields += "; "; + ++index; + if (index + 1 < fields_value.size()) + cached_fields += ";"; } return cached_fields; diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp index ce3d07ce..9876a109 100644 --- a/test/unit/server/helmet.cpp +++ b/test/unit/server/helmet.cpp @@ -27,7 +27,7 @@ struct helmet_test helmet helmet{opt}; route_params p; - p.req.append("X-Download-Options", "noopen"); + p.res.append("X-Download-Options", "noopen"); auto ec = helmet(p); BOOST_TEST(ec == route::next); From a0a88fa6ddaa169c0fefe8608d9f40bf0e10a1bf Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 12:37:37 +0100 Subject: [PATCH 09/26] feat: add `allow_list` alias for `sp` namespace. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index fb83144b..247bc4b9 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -116,6 +116,8 @@ inline option_pair content_security_policy(const std::vector& allow /** @brief Security Policy namespace for Content-Security-Policy */ namespace sp { + using allow_list = std::vector; + enum class allow_type { self, none, unsafe_inline }; inline void push_back(std::vector& allow_list, const core::string_view& allow, const allow_type& type) From 033353782be0779755d9e973349cd3534e2dfb8a Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 12:59:33 +0100 Subject: [PATCH 10/26] feat: update enum allow_type to csp_type. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 95 ++++++------------- src/server/helmet.cpp | 105 +++++++++++++++++++++ 2 files changed, 133 insertions(+), 67 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 247bc4b9..f3e8afca 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -68,83 +68,44 @@ class helmet enum class helmet_download_type { noopen, disabled }; enum class helmet_origin_type { deny, sameorigin }; +enum class csp_type { self, none, unsafe_inline, sandbox }; + +using csp_allow_list = std::vector; + +/** @brief Security Policy builder for CSP headers. */ +struct security_policy +{ + csp_allow_list list; + + BOOST_HTTP_PROTO_DECL + security_policy(); + + BOOST_HTTP_PROTO_DECL + ~security_policy(); + + BOOST_HTTP_PROTO_DECL + security_policy& append(const core::string_view& allow, + const csp_type& type); + + BOOST_HTTP_PROTO_DECL + security_policy& remove(const core::string_view& allow); +}; + using option_pair = std::pair>; /** @brief Enable the X-Download-Options header. */ -inline option_pair x_download_options(const helmet_download_type& type) -{ - if (type == helmet_download_type::noopen) - { - return {"X-Download-Options", {"noopen"}}; - } - - return {}; -} +option_pair x_download_options(const helmet_download_type& type); /** @brief Enable the X-Frame-Origin header */ -inline option_pair x_frame_origin(const helmet_origin_type& origin) -{ - if (origin == helmet_origin_type::sameorigin) - { - return {"X-Frame-Options", {"SAMEORIGIN"}}; - } - else if (origin == helmet_origin_type::deny) - { - return {"X-Frame-Options", {"DENY"}}; - } - - return {}; -} +option_pair x_frame_origin(const helmet_origin_type& origin); -inline option_pair x_xss_protection() -{ - return {"X-XSS-Protection", {"0"}}; -} +option_pair x_xss_protection(); +option_pair x_content_type_options(); -inline option_pair x_content_type_options() -{ - return {"X-Content-Type-Options", {"nosniff"}}; -} - -inline option_pair content_security_policy(const std::vector& allow_list) -{ - return {"Content-Security-Policy", allow_list}; -} +option_pair content_security_policy(const security_policy& sp); -/** @brief Security Policy namespace for Content-Security-Policy */ -namespace sp -{ - using allow_list = std::vector; - - enum class allow_type { self, none, unsafe_inline }; - - inline void push_back(std::vector& allow_list, const core::string_view& allow, const allow_type& type) - { - if (std::find(allow_list.cbegin(), allow_list.cend(), allow) == allow_list.cend()) - { - std::string final = allow; - - if (type == allow_type::self) - final += " 'self'"; - else if (type == allow_type::unsafe_inline) - final += " 'unsafe-inline'"; - else - final += " 'none'"; - - allow_list.emplace_back(final); - } - } - - inline void remove(std::vector& allow_list, const core::string_view& allow) - { - if (auto it = std::find(allow_list.cbegin(), allow_list.cend(), allow); it != allow_list.cend()) - { - allow_list.erase(it); - } - } -} } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 85b4a69b..cb6d1d55 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -93,6 +93,9 @@ helmet( }); } +/** + AMLALE: This is here to prevent compiler erros from unique_ptr. (destructor doesnn't know how to destroy impl) +*/ helmet::~helmet() = default; route_result @@ -108,6 +111,108 @@ operator()( return route::next; } + +security_policy& security_policy::append(const core::string_view& allow, const csp_type& type) +{ + if (std::find(list.cbegin(), list.cend(), allow) == list.cend()) + { + std::string final = allow; + + switch (type) + { + case csp_type::sandbox: + { + final += " sandbox"; + break; + } + case csp_type::none: + { + final += " none"; + break; + } + case csp_type::unsafe_inline: + { + final += " unsafe_inline"; + break; + } + case csp_type::self: + { + final += " self"; + break; + } + default: + detail::throw_invalid_argument(); + return *this; + } + + list.emplace_back(final); + } + else + { + detail::throw_bad_alloc(); + } + + return *this; +} + +security_policy::security_policy() = default; +security_policy::~security_policy() = default; + +security_policy& security_policy::remove(const core::string_view& allow) +{ + if (auto it = std::find(list.cbegin(), list.cend(), allow); it != list.cend()) + { + list.erase(it); + } + else + { + detail::throw_invalid_argument(); + } + + return *this; +} + +option_pair x_xss_protection() +{ + return {"X-XSS-Protection", {"0"}}; +} + +option_pair x_content_type_options() +{ + return {"X-Content-Type-Options", {"nosniff"}}; +} + +option_pair content_security_policy(const security_policy& sp) +{ + if (sp.list.empty()) + detail::throw_invalid_argument(); + + return {"Content-Security-Policy", sp.list}; +} + +option_pair x_frame_origin(const helmet_origin_type& origin) +{ + if (origin == helmet_origin_type::sameorigin) + { + return {"X-Frame-Options", {"SAMEORIGIN"}}; + } + else if (origin == helmet_origin_type::deny) + { + return {"X-Frame-Options", {"DENY"}}; + } + + return {}; +} + +option_pair x_download_options(const helmet_download_type& type) +{ + if (type == helmet_download_type::noopen) + { + return {"X-Download-Options", {"noopen"}}; + } + + return {}; +} } } From 7a605282ae716a2afbb1fdf5e9e382ea07fc6d8f Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 16:28:20 +0100 Subject: [PATCH 11/26] fix: helmet.hpp: pimpl implementation fixes. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 7 ++++-- src/server/helmet.cpp | 27 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index f3e8afca..54cf4337 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -41,8 +41,8 @@ struct helmet_options class helmet { struct impl; - std::unique_ptr impl_; - + impl* impl_; + public: /** @brief Builds an helmet and compute its options for caching purposes. @@ -54,6 +54,9 @@ class helmet BOOST_HTTP_PROTO_DECL ~helmet(); + helmet& operator=(helmet&&) noexcept; + helmet(helmet&&) noexcept; + /** @brief Iterates over cachedHeaders and apply its rules to the response params. @param p route parameter argument diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index cb6d1d55..d9be379c 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -81,8 +81,8 @@ struct helmet::impl helmet:: helmet( helmet_options helmet_options) + : impl_(new impl()) { - impl_ = std::make_unique(); impl_->options_ = std::move(helmet_options); std::for_each(impl_->options_.headers.begin(), impl_->options_.headers.end(), @@ -93,10 +93,27 @@ helmet( }); } -/** - AMLALE: This is here to prevent compiler erros from unique_ptr. (destructor doesnn't know how to destroy impl) -*/ -helmet::~helmet() = default; +helmet::~helmet() +{ + delete impl_; + impl_ = nullptr; +} + +helmet& helmet::operator=(helmet&& other) noexcept +{ + impl_ = other.impl_; + other.impl_ = nullptr; + + return *this; +} + + +helmet:: +helmet(helmet&& other) noexcept + : impl_(other.impl_) +{ + other.impl_ = nullptr; +} route_result helmet:: From 8e48f60154a9987c208a2a799db1adb55f8c42b1 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 17:30:30 +0100 Subject: [PATCH 12/26] feat: helmet: support src directives. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 4 ++++ src/server/helmet.cpp | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 54cf4337..eacc87ab 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -90,6 +90,10 @@ struct security_policy security_policy& append(const core::string_view& allow, const csp_type& type); + BOOST_HTTP_PROTO_DECL + security_policy& append(const core::string_view& allow, + const core::string_view& source); + BOOST_HTTP_PROTO_DECL security_policy& remove(const core::string_view& allow); }; diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index d9be379c..eac03c79 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -129,6 +129,23 @@ operator()( return route::next; } +security_policy& security_policy::append(const core::string_view& allow, + const core::string_view& source) +{ + if (allow.empty()) + detail::throw_invalid_argument(); + + std::string final = allow; + + if (source.empty()) + detail::throw_invalid_argument(); + + final += source; + + list.emplace_back(final); + return *this; +} + security_policy& security_policy::append(const core::string_view& allow, const csp_type& type) { if (std::find(list.cbegin(), list.cend(), allow) == list.cend()) From a658a88d50a18bbea4b69f21ea7df7fd78c8b8df Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 17:42:53 +0100 Subject: [PATCH 13/26] feat: append for `urls::url_view` inside security_policy builder. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 6 +++--- src/server/helmet.cpp | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index eacc87ab..aec0b2e3 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -91,11 +91,11 @@ struct security_policy const csp_type& type); BOOST_HTTP_PROTO_DECL - security_policy& append(const core::string_view& allow, - const core::string_view& source); + security_policy& remove(const core::string_view& allow); BOOST_HTTP_PROTO_DECL - security_policy& remove(const core::string_view& allow); + security_policy& append(const core::string_view& allow, + const urls::url_view& source); }; using option_pair = std::pair>; diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index eac03c79..31597ae6 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -24,14 +24,10 @@ namespace detail { std::string cached_fields; - std::size_t index{}; - for (const auto& field : fields_value) { cached_fields += field; - ++index; - if (index + 1 < fields_value.size()) - cached_fields += ";"; + cached_fields += ";"; } return cached_fields; @@ -130,7 +126,7 @@ operator()( } security_policy& security_policy::append(const core::string_view& allow, - const core::string_view& source) + const urls::url_view& source) { if (allow.empty()) detail::throw_invalid_argument(); @@ -140,7 +136,7 @@ security_policy& security_policy::append(const core::string_view& allow, if (source.empty()) detail::throw_invalid_argument(); - final += source; + final += source.data(); list.emplace_back(final); return *this; From 5c491c17e23c69cbee4b436f4a1bd991c083a34e Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 17:47:26 +0100 Subject: [PATCH 14/26] feat: src/server/helmet.cpp: add space for csp values. Signed-off-by: Amlal El Mahrouss --- src/server/helmet.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 31597ae6..2696036a 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -136,6 +136,7 @@ security_policy& security_policy::append(const core::string_view& allow, if (source.empty()) detail::throw_invalid_argument(); + final.push_back(' '); final += source.data(); list.emplace_back(final); @@ -190,7 +191,9 @@ security_policy::~security_policy() = default; security_policy& security_policy::remove(const core::string_view& allow) { - if (auto it = std::find(list.cbegin(), list.cend(), allow); it != list.cend()) + if (auto it = std::find_if(list.cbegin(), list.cend(), [&allow](const std::string& in) { + return in.find(allow) != std::string::npos; + }); it != list.cend()) { list.erase(it); } From 1b8a47c0803b319819e8d921c73c2a545bbb6a62 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 18:01:32 +0100 Subject: [PATCH 15/26] feat: move csp_policy to helmet to avoid name clashes. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 55 +++++++++++----------- src/server/helmet.cpp | 12 ++--- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index aec0b2e3..1006e063 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -35,6 +35,15 @@ struct helmet_options ~helmet_options(); }; +enum class helmet_download_type { noopen, disabled }; +enum class helmet_origin_type { deny, sameorigin }; + +enum class csp_type { self, none, unsafe_inline, sandbox }; + +using csp_allow_list = std::vector; + +using option_pair = std::pair>; + /** @brief Security middleware inspired by express.js concept of helmets. */ @@ -66,40 +75,30 @@ class helmet route_result operator()(route_params& p) const; -}; - -enum class helmet_download_type { noopen, disabled }; -enum class helmet_origin_type { deny, sameorigin }; - -enum class csp_type { self, none, unsafe_inline, sandbox }; + /** @brief Security Policy builder for CSP headers. */ + struct csp_policy + { + csp_allow_list list; -using csp_allow_list = std::vector; + BOOST_HTTP_PROTO_DECL + csp_policy(); -/** @brief Security Policy builder for CSP headers. */ -struct security_policy -{ - csp_allow_list list; + BOOST_HTTP_PROTO_DECL + ~csp_policy(); - BOOST_HTTP_PROTO_DECL - security_policy(); + BOOST_HTTP_PROTO_DECL + csp_policy& append(const core::string_view& allow, + const csp_type& type); - BOOST_HTTP_PROTO_DECL - ~security_policy(); + BOOST_HTTP_PROTO_DECL + csp_policy& remove(const core::string_view& allow); - BOOST_HTTP_PROTO_DECL - security_policy& append(const core::string_view& allow, - const csp_type& type); - - BOOST_HTTP_PROTO_DECL - security_policy& remove(const core::string_view& allow); - - BOOST_HTTP_PROTO_DECL - security_policy& append(const core::string_view& allow, - const urls::url_view& source); + BOOST_HTTP_PROTO_DECL + csp_policy& append(const core::string_view& allow, + const urls::url_view& source); + }; }; -using option_pair = std::pair>; - /** @brief Enable the X-Download-Options header. */ @@ -111,7 +110,7 @@ option_pair x_frame_origin(const helmet_origin_type& origin); option_pair x_xss_protection(); option_pair x_content_type_options(); -option_pair content_security_policy(const security_policy& sp); +option_pair content_security_policy(const helmet::csp_policy& sp); } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 2696036a..d9228e20 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -125,7 +125,7 @@ operator()( return route::next; } -security_policy& security_policy::append(const core::string_view& allow, +helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, const urls::url_view& source) { if (allow.empty()) @@ -143,7 +143,7 @@ security_policy& security_policy::append(const core::string_view& allow, return *this; } -security_policy& security_policy::append(const core::string_view& allow, const csp_type& type) +helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, const csp_type& type) { if (std::find(list.cbegin(), list.cend(), allow) == list.cend()) { @@ -186,10 +186,10 @@ security_policy& security_policy::append(const core::string_view& allow, const c return *this; } -security_policy::security_policy() = default; -security_policy::~security_policy() = default; +helmet::csp_policy::csp_policy() = default; +helmet::csp_policy::~csp_policy() = default; -security_policy& security_policy::remove(const core::string_view& allow) +helmet::csp_policy& helmet::csp_policy::remove(const core::string_view& allow) { if (auto it = std::find_if(list.cbegin(), list.cend(), [&allow](const std::string& in) { return in.find(allow) != std::string::npos; @@ -215,7 +215,7 @@ option_pair x_content_type_options() return {"X-Content-Type-Options", {"nosniff"}}; } -option_pair content_security_policy(const security_policy& sp) +option_pair content_security_policy(const helmet::csp_policy& sp) { if (sp.list.empty()) detail::throw_invalid_argument(); From 4e4936fc57b15e452962cc8c9b5844f533d9ea77 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 18:22:01 +0100 Subject: [PATCH 16/26] feat: complete helmet implementation and documentation. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 438 +++++++++++++++++++-- src/server/helmet.cpp | 211 +++++++++- test/unit/server/helmet.cpp | 271 ++++++++++++- 3 files changed, 885 insertions(+), 35 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 1006e063..034beb1b 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -17,35 +17,255 @@ namespace boost { namespace http_proto { +/** Configuration options for helmet security middleware. + + This structure holds a collection of HTTP security headers + that will be applied to responses. Headers are stored as + key-value pairs where each key can have multiple values. + + @see helmet +*/ struct helmet_options { using pair_type = std::pair>; using map_type = std::vector; - + + /** Collection of security headers to apply */ map_type headers; - /** - @brief Sets the value at a specific location. - @param helmet_hdr the indexable header. - @return helmet_options instance. + /** Set or update a security header. + + If a header with the same name already exists, it will be replaced. + Otherwise, the new header is appended to the collection. + + @param helmet_hdr The header name and values to set. + + @return A reference to this object for chaining. */ helmet_options& set(const pair_type& helmet_hdr); + /** Construct helmet options with secure defaults. + + Initializes the following headers: + - X-Download-Options: noopen + - X-Frame-Options: DENY + - X-XSS-Protection: 0 + - X-Content-Type-Options: nosniff + - Strict-Transport-Security + - Cross-Origin-Opener-Policy + - Cross-Origin-Resource-Policy + - Origin-Agent-Cluster + - Referrer-Policy: no-referrer + - X-DNS-Prefetch-Control: off + - X-Permitted-Cross-Domain-Policies: none + - Removes X-Powered-By header + */ helmet_options(); + ~helmet_options(); }; -enum class helmet_download_type { noopen, disabled }; -enum class helmet_origin_type { deny, sameorigin }; +/** Download behavior for X-Download-Options header. + + Controls how browsers handle file downloads. +*/ +enum class helmet_download_type +{ + /** Prevent opening files directly in IE8+ */ + noopen, + + /** Disable the X-Download-Options header */ + disabled +}; + +/** Frame origin policy for X-Frame-Options header. + + Controls whether the page can be embedded in frames. +*/ +enum class helmet_origin_type +{ + /** Prevent all framing */ + deny, + + /** Allow framing from same origin only */ + sameorigin +}; + +/** Content Security Policy directive values. + + Defines standard CSP source expressions. + + @see helmet::csp_policy +*/ +enum class csp_type +{ + /** Refers to the current origin */ + self, + + /** Blocks all sources */ + none, + + /** Allows inline scripts/styles */ + unsafe_inline, + + /** Allows eval() and similar */ + unsafe_eval, + + /** Enables strict dynamic mode */ + strict_dynamic, + + /** Enables sandbox restrictions */ + sandbox, + + /** Includes samples in violation reports */ + report_sample +}; + +/** Cross-Origin-Opener-Policy values. + + Controls cross-origin window isolation. +*/ +enum class coop_policy_type +{ + /** No isolation (default browser behavior) */ + unsafe_none, + + /** Allow popups from same origin */ + same_origin_allow_popups, + + /** Strict same-origin isolation */ + same_origin +}; + +/** Cross-Origin-Resource-Policy values. + + Controls cross-origin resource sharing. +*/ +enum class corp_policy_type +{ + /** Only same-origin requests allowed */ + same_origin, + + /** Same-site requests allowed */ + same_site, + + /** Cross-origin requests allowed */ + cross_origin +}; + +/** Cross-Origin-Embedder-Policy values. + + Controls embedding of cross-origin resources. +*/ +enum class coep_policy_type +{ + /** No restrictions (default) */ + unsafe_none, + + /** Require CORP header on cross-origin resources */ + require_corp, + + /** Load cross-origin resources without credentials */ + credentialless +}; + +/** Referrer-Policy values. + + Controls how much referrer information is sent with requests. +*/ +enum class referrer_policy_type +{ + /** Never send referrer */ + no_referrer, + + /** Send referrer for HTTPS→HTTPS, not HTTPS→HTTP */ + no_referrer_when_downgrade, + + /** Send referrer for same-origin requests only */ + same_origin, + + /** Send origin only */ + origin, + + /** Send origin only for HTTPS→HTTPS */ + strict_origin, + + /** Send full URL for same-origin, origin for cross-origin */ + origin_when_cross_origin, + + /** Send origin only for HTTPS→HTTPS, nothing for HTTPS→HTTP */ + strict_origin_when_cross_origin, + + /** Always send full URL (unsafe) */ + unsafe_url +}; + +/** X-Permitted-Cross-Domain-Policies values. -enum class csp_type { self, none, unsafe_inline, sandbox }; + Controls cross-domain policy files (Flash, Acrobat). +*/ +enum class cross_domain_policy_type +{ + /** No policy files allowed */ + none, + + /** Only master policy file */ + master_only, + + /** Policy files with matching content-type */ + by_content_type, + + /** Policy files via FTP filename */ + by_ftp_filename, + + /** All policy files allowed */ + all +}; +/** HTTP Strict Transport Security options. + + Configures the Strict-Transport-Security header. + + @see strict_transport_security +*/ +struct hsts_options +{ + /** Maximum age in seconds (default: 1 year) */ + std::size_t max_age = 31536000; + + /** Apply to all subdomains */ + bool include_sub_domains = true; + + /** Request preload list inclusion */ + bool preload = false; +}; + +/** List of allowed sources for CSP directives */ using csp_allow_list = std::vector; +/** Header name and values pair */ using option_pair = std::pair>; -/** - @brief Security middleware inspired by express.js concept of helmets. +/** Security middleware inspired by helmet.js. + + Helmet helps secure HTTP responses by setting various + HTTP security headers. It provides protection against + common web vulnerabilities like XSS, clickjacking, + and other attacks. + + @par Example + @code + helmet_options opts; + opts.set(x_frame_origin(helmet_origin_type::deny)); + + helmet::csp_policy csp; + csp.append("script-src", csp_type::self); + opts.set(content_security_policy(csp)); + + srv.wwwroot.use(helmet(opts)); + @endcode + + @see helmet_options */ class helmet { @@ -53,8 +273,15 @@ class helmet impl* impl_; public: - /** - @brief Builds an helmet and compute its options for caching purposes. + /** Construct helmet middleware with specified options. + + The constructor pre-computes and caches all header + values for efficient application to responses. + + @param options Security header configuration. If not + provided, secure defaults are used. + + @see helmet_options */ BOOST_HTTP_PROTO_DECL explicit helmet( @@ -66,18 +293,36 @@ class helmet helmet& operator=(helmet&&) noexcept; helmet(helmet&&) noexcept; - /** - @brief Iterates over cachedHeaders and apply its rules to the response params. - @param p route parameter argument - @return route_result an error_code signaling the route's status. + /** Apply security headers to the response. + + Iterates through all configured headers and applies them + to the response. Empty header values indicate headers + that should be removed (e.g., X-Powered-By). + + @param p Route parameters containing the response to modify. + + @return Always returns route::next to continue processing. */ BOOST_HTTP_PROTO_DECL route_result operator()(route_params& p) const; - /** @brief Security Policy builder for CSP headers. */ + /** Content Security Policy builder. + + Provides a fluent interface for constructing CSP headers + with multiple directives and sources. + + @par Example + @code + helmet::csp_policy policy; + policy.append("script-src", csp_type::self) + .append("style-src", "https://example.com") + .append("img-src", csp_type::none); + @endcode + */ struct csp_policy { + /** List of CSP directives */ csp_allow_list list; BOOST_HTTP_PROTO_DECL @@ -86,32 +331,181 @@ class helmet BOOST_HTTP_PROTO_DECL ~csp_policy(); + /** Append a CSP directive with a predefined type. + + @param allow The directive name (e.g., "script-src"). + @param type The CSP source expression type. + + @return A reference to this object for chaining. + + @throws std::invalid_argument if the directive already exists. + */ BOOST_HTTP_PROTO_DECL - csp_policy& append(const core::string_view& allow, + csp_policy& append(const core::string_view& allow, const csp_type& type); + /** Remove a CSP directive. + + @param allow The directive name to remove. + + @return A reference to this object for chaining. + + @throws std::invalid_argument if the directive is not found. + */ BOOST_HTTP_PROTO_DECL csp_policy& remove(const core::string_view& allow); + /** Append a CSP directive with a URL source. + + @param allow The directive name (e.g., "script-src"). + @param source The URL to allow. + + @return A reference to this object for chaining. + + @throws std::invalid_argument if parameters are empty. + */ BOOST_HTTP_PROTO_DECL - csp_policy& append(const core::string_view& allow, + csp_policy& append(const core::string_view& allow, const urls::url_view& source); }; }; -/** - @brief Enable the X-Download-Options header. +/** Return X-Download-Options header configuration. + + Controls file download behavior in Internet Explorer 8+. + + @param type The download restriction type. + + @return Header name and value pair. */ option_pair x_download_options(const helmet_download_type& type); -/** @brief Enable the X-Frame-Origin header */ +/** Return X-Frame-Options header configuration. + + Prevents clickjacking attacks by controlling frame embedding. + + @param origin The frame embedding policy. + + @return Header name and value pair. +*/ option_pair x_frame_origin(const helmet_origin_type& origin); +/** Return X-XSS-Protection header configuration. + + Disables legacy XSS filtering which can create vulnerabilities. + Modern browsers rely on CSP instead. + + @return Header name and value pair (always "0"). +*/ option_pair x_xss_protection(); + +/** Return X-Content-Type-Options header configuration. + + Prevents MIME-type sniffing attacks. + + @return Header name and value pair (always "nosniff"). +*/ option_pair x_content_type_options(); +/** Return Content-Security-Policy header configuration. + + Configures CSP to prevent XSS and data injection attacks. + + @param sp The CSP policy with directives. + + @return Header name and value pair. + + @throws std::invalid_argument if the policy is empty. +*/ option_pair content_security_policy(const helmet::csp_policy& sp); +/** Return Strict-Transport-Security header configuration. + + Enforces HTTPS connections to prevent downgrade attacks. + + @param options HSTS configuration parameters. + + @return Header name and value pair. +*/ +option_pair strict_transport_security(const hsts_options& options = hsts_options{}); + +/** Return Cross-Origin-Opener-Policy header configuration. + + Controls cross-origin window isolation. + + @param policy The COOP policy type. + + @return Header name and value pair. +*/ +option_pair cross_origin_opener_policy(const coop_policy_type& policy = coop_policy_type::same_origin); + +/** Return Cross-Origin-Resource-Policy header configuration. + + Controls cross-origin resource sharing. + + @param policy The CORP policy type. + + @return Header name and value pair. +*/ +option_pair cross_origin_resource_policy(const corp_policy_type& policy = corp_policy_type::same_origin); + +/** Return Cross-Origin-Embedder-Policy header configuration. + + Controls embedding of cross-origin resources. + + @param policy The COEP policy type. + + @return Header name and value pair. +*/ +option_pair cross_origin_embedder_policy(const coep_policy_type& policy = coep_policy_type::require_corp); + +/** Return Referrer-Policy header configuration. + + Controls how much referrer information is sent with requests. + + @param policy The referrer policy type. + + @return Header name and value pair. +*/ +option_pair referrer_policy(const referrer_policy_type& policy = referrer_policy_type::no_referrer); + +/** Return Origin-Agent-Cluster header configuration. + + Requests origin-keyed agent clusters for better isolation. + + @return Header name and value pair (always "?1"). +*/ +option_pair origin_agent_cluster(); + +/** Return X-DNS-Prefetch-Control header configuration. + + Controls DNS prefetching to balance performance and privacy. + + @param allow Whether to enable DNS prefetching. + + @return Header name and value pair. +*/ +option_pair dns_prefetch_control(bool allow = false); + +/** Return X-Permitted-Cross-Domain-Policies header configuration. + + Controls cross-domain policy files for Flash and Adobe Acrobat. + + @param policy The cross-domain policy type. + + @return Header name and value pair. +*/ +option_pair permitted_cross_domain_policies(const cross_domain_policy_type& policy = cross_domain_policy_type::none); + +/** Return configuration to remove X-Powered-By header. + + Removes the X-Powered-By header to avoid revealing + server technology details. + + @return Header name with empty value (triggers removal). +*/ +option_pair hide_powered_by(); + } } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index d9228e20..0b0c9593 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -35,12 +35,21 @@ namespace detail { } -helmet_options::helmet_options() +helmet_options::helmet_options() { + // Default headers as per helmet.js defaults this->set(x_download_options(helmet_download_type::noopen)); this->set(x_frame_origin(helmet_origin_type::deny)); this->set(x_xss_protection()); this->set(x_content_type_options()); + this->set(strict_transport_security()); + this->set(cross_origin_opener_policy()); + this->set(cross_origin_resource_policy()); + this->set(origin_agent_cluster()); + this->set(referrer_policy()); + this->set(dns_prefetch_control()); + this->set(permitted_cross_domain_policies()); + this->set(hide_powered_by()); } helmet_options::~helmet_options() = default; @@ -116,10 +125,18 @@ helmet:: operator()( route_params& p) const { - std::for_each(impl_->cached_headers_.begin(), impl_->cached_headers_.end(), - [&p] (const impl::pair_type& hdr) + std::for_each(impl_->cached_headers_.begin(), impl_->cached_headers_.end(), + [&p] (const impl::pair_type& hdr) { - p.res.set(hdr.first, hdr.second); + // If value is empty, it means we should remove the header (e.g., X-Powered-By) + if (hdr.second.empty()) + { + p.res.erase(hdr.first); + } + else + { + p.res.set(hdr.first, hdr.second); + } }); return route::next; @@ -153,22 +170,37 @@ helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, c { case csp_type::sandbox: { - final += " sandbox"; + final += " 'sandbox'"; break; } case csp_type::none: { - final += " none"; + final += " 'none'"; break; } case csp_type::unsafe_inline: { - final += " unsafe_inline"; + final += " 'unsafe-inline'"; + break; + } + case csp_type::unsafe_eval: + { + final += " 'unsafe-eval'"; + break; + } + case csp_type::strict_dynamic: + { + final += " 'strict-dynamic'"; + break; + } + case csp_type::report_sample: + { + final += " 'report-sample'"; break; } case csp_type::self: { - final += " self"; + final += " 'self'"; break; } default: @@ -246,6 +278,169 @@ option_pair x_download_options(const helmet_download_type& type) return {}; } + +option_pair strict_transport_security(const hsts_options& options) +{ + std::string value = "max-age=" + std::to_string(options.max_age); + + if (options.include_sub_domains) + { + value += "; includeSubDomains"; + } + + if (options.preload) + { + value += "; preload"; + } + + return {"Strict-Transport-Security", {value}}; +} + +option_pair cross_origin_opener_policy(const coop_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case coop_policy_type::unsafe_none: + value = "unsafe-none"; + break; + case coop_policy_type::same_origin_allow_popups: + value = "same-origin-allow-popups"; + break; + case coop_policy_type::same_origin: + value = "same-origin"; + break; + default: + return {}; + } + + return {"Cross-Origin-Opener-Policy", {value}}; +} + +option_pair cross_origin_resource_policy(const corp_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case corp_policy_type::same_origin: + value = "same-origin"; + break; + case corp_policy_type::same_site: + value = "same-site"; + break; + case corp_policy_type::cross_origin: + value = "cross-origin"; + break; + default: + return {}; + } + + return {"Cross-Origin-Resource-Policy", {value}}; +} + +option_pair cross_origin_embedder_policy(const coep_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case coep_policy_type::unsafe_none: + value = "unsafe-none"; + break; + case coep_policy_type::require_corp: + value = "require-corp"; + break; + case coep_policy_type::credentialless: + value = "credentialless"; + break; + default: + return {}; + } + + return {"Cross-Origin-Embedder-Policy", {value}}; +} + +option_pair referrer_policy(const referrer_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case referrer_policy_type::no_referrer: + value = "no-referrer"; + break; + case referrer_policy_type::no_referrer_when_downgrade: + value = "no-referrer-when-downgrade"; + break; + case referrer_policy_type::same_origin: + value = "same-origin"; + break; + case referrer_policy_type::origin: + value = "origin"; + break; + case referrer_policy_type::strict_origin: + value = "strict-origin"; + break; + case referrer_policy_type::origin_when_cross_origin: + value = "origin-when-cross-origin"; + break; + case referrer_policy_type::strict_origin_when_cross_origin: + value = "strict-origin-when-cross-origin"; + break; + case referrer_policy_type::unsafe_url: + value = "unsafe-url"; + break; + default: + return {}; + } + + return {"Referrer-Policy", {value}}; +} + +option_pair origin_agent_cluster() +{ + return {"Origin-Agent-Cluster", {"?1"}}; +} + +option_pair dns_prefetch_control(bool allow) +{ + return {"X-DNS-Prefetch-Control", {allow ? "on" : "off"}}; +} + +option_pair permitted_cross_domain_policies(const cross_domain_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case cross_domain_policy_type::none: + value = "none"; + break; + case cross_domain_policy_type::master_only: + value = "master-only"; + break; + case cross_domain_policy_type::by_content_type: + value = "by-content-type"; + break; + case cross_domain_policy_type::by_ftp_filename: + value = "by-ftp-filename"; + break; + case cross_domain_policy_type::all: + value = "all"; + break; + default: + return {}; + } + + return {"X-Permitted-Cross-Domain-Policies", {value}}; +} + +option_pair hide_powered_by() +{ + return {"X-Powered-By", {}}; +} } } diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp index 9876a109..fc82b8c9 100644 --- a/test/unit/server/helmet.cpp +++ b/test/unit/server/helmet.cpp @@ -15,22 +15,283 @@ namespace http_proto { struct helmet_test { - void - run() + void + run() { - // X-Download-Options + // Test X-Download-Options { helmet_options opt; - + opt.set(x_download_options(helmet_download_type::noopen)); helmet helmet{opt}; route_params p; - p.res.append("X-Download-Options", "noopen"); auto ec = helmet(p); BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Download-Options") > 0); + } + + // Test X-Frame-Options with DENY + { + helmet_options opt; + + opt.set(x_frame_origin(helmet_origin_type::deny)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Frame-Options") > 0); + } + + // Test X-Frame-Options with SAMEORIGIN + { + helmet_options opt; + + opt.set(x_frame_origin(helmet_origin_type::sameorigin)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Frame-Options") > 0); + } + + // Test X-XSS-Protection + { + helmet_options opt; + + opt.set(x_xss_protection()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-XSS-Protection") > 0); + } + + // Test X-Content-Type-Options + { + helmet_options opt; + + opt.set(x_content_type_options()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Content-Type-Options") > 0); + } + + // Test Strict-Transport-Security with defaults + { + helmet_options opt; + + opt.set(strict_transport_security()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Strict-Transport-Security") > 0); + } + + // Test Strict-Transport-Security with custom options + { + helmet_options opt; + hsts_options hsts; + hsts.max_age = 86400; + hsts.include_sub_domains = true; + hsts.preload = true; + + opt.set(strict_transport_security(hsts)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Strict-Transport-Security") > 0); + } + + // Test Cross-Origin-Opener-Policy + { + helmet_options opt; + + opt.set(cross_origin_opener_policy(coop_policy_type::same_origin)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Cross-Origin-Opener-Policy") > 0); + } + + // Test Cross-Origin-Resource-Policy + { + helmet_options opt; + + opt.set(cross_origin_resource_policy(corp_policy_type::same_origin)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Cross-Origin-Resource-Policy") > 0); + } + + // Test Cross-Origin-Embedder-Policy + { + helmet_options opt; + + opt.set(cross_origin_embedder_policy(coep_policy_type::require_corp)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Cross-Origin-Embedder-Policy") > 0); + } + + // Test Referrer-Policy + { + helmet_options opt; + + opt.set(referrer_policy(referrer_policy_type::no_referrer)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Referrer-Policy") > 0); + } + + // Test Origin-Agent-Cluster + { + helmet_options opt; + + opt.set(origin_agent_cluster()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Origin-Agent-Cluster") > 0); + } + + // Test X-DNS-Prefetch-Control + { + helmet_options opt; + + opt.set(dns_prefetch_control(false)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-DNS-Prefetch-Control") > 0); + } + + // Test X-Permitted-Cross-Domain-Policies + { + helmet_options opt; + + opt.set(permitted_cross_domain_policies(cross_domain_policy_type::none)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Permitted-Cross-Domain-Policies") > 0); + } + + // Test hide_powered_by + { + helmet_options opt; + + opt.set(hide_powered_by()); + + helmet helmet{opt}; + route_params p; + + // Add X-Powered-By header first + p.res.set("X-Powered-By", "Express"); + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + // After helmet middleware, X-Powered-By should be removed + BOOST_TEST(p.res.count("X-Powered-By") == 0); + } + + // Test Content-Security-Policy with CSP builder + { + helmet_options opt; + helmet::csp_policy csp; + + csp.append("default-src", csp_type::self); + csp.append("script-src", csp_type::self); + csp.append("style-src", csp_type::self); + + opt.set(content_security_policy(csp)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Content-Security-Policy") > 0); + } + + // Test default helmet configuration + { + helmet_options opt; + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + // Check that default headers are set + BOOST_TEST(p.res.count("X-Download-Options") > 0); + BOOST_TEST(p.res.count("X-Frame-Options") > 0); + BOOST_TEST(p.res.count("X-XSS-Protection") > 0); + BOOST_TEST(p.res.count("X-Content-Type-Options") > 0); + BOOST_TEST(p.res.count("Strict-Transport-Security") > 0); + BOOST_TEST(p.res.count("Cross-Origin-Opener-Policy") > 0); + BOOST_TEST(p.res.count("Cross-Origin-Resource-Policy") > 0); + BOOST_TEST(p.res.count("Origin-Agent-Cluster") > 0); + BOOST_TEST(p.res.count("Referrer-Policy") > 0); + BOOST_TEST(p.res.count("X-DNS-Prefetch-Control") > 0); + BOOST_TEST(p.res.count("X-Permitted-Cross-Domain-Policies") > 0); } } }; From 9b5e9f2f2b8d16d812a56d728f70ab6a50060607 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Wed, 24 Dec 2025 18:24:33 +0100 Subject: [PATCH 17/26] fix: helmet.cpp: don't use `final` in variable names. Signed-off-by: Amlal El Mahrouss --- src/server/helmet.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 0b0c9593..abaa7fa6 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -148,15 +148,15 @@ helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, if (allow.empty()) detail::throw_invalid_argument(); - std::string final = allow; + std::string value = allow; if (source.empty()) detail::throw_invalid_argument(); - final.push_back(' '); - final += source.data(); + value.push_back(' '); + value += source.data(); - list.emplace_back(final); + list.emplace_back(value); return *this; } @@ -164,43 +164,43 @@ helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, c { if (std::find(list.cbegin(), list.cend(), allow) == list.cend()) { - std::string final = allow; + std::string value = allow; switch (type) { case csp_type::sandbox: { - final += " 'sandbox'"; + value += " 'sandbox'"; break; } case csp_type::none: { - final += " 'none'"; + value += " 'none'"; break; } case csp_type::unsafe_inline: { - final += " 'unsafe-inline'"; + value += " 'unsafe-inline'"; break; } case csp_type::unsafe_eval: { - final += " 'unsafe-eval'"; + value += " 'unsafe-eval'"; break; } case csp_type::strict_dynamic: { - final += " 'strict-dynamic'"; + value += " 'strict-dynamic'"; break; } case csp_type::report_sample: { - final += " 'report-sample'"; + value += " 'report-sample'"; break; } case csp_type::self: { - final += " 'self'"; + value += " 'self'"; break; } default: @@ -208,7 +208,7 @@ helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, c return *this; } - list.emplace_back(final); + list.emplace_back(value); } else { From d91ebfa71a4929c26c9228b874d75ba3b0de1c91 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 09:24:08 +0100 Subject: [PATCH 18/26] feat: helmet.cpp: append method should append a new or to a existing directive. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 5 +- src/server/helmet.cpp | 126 +++++++++++++-------- 2 files changed, 78 insertions(+), 53 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 034beb1b..7352d595 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -36,8 +36,7 @@ struct helmet_options /** Set or update a security header. If a header with the same name already exists, it will be replaced. - Otherwise, the new header is appended to the collection. - + Otherwise, the new header is appended to the collection. @param helmet_hdr The header name and values to set. @return A reference to this object for chaining. @@ -64,7 +63,7 @@ struct helmet_options ~helmet_options(); }; - + /** Download behavior for X-Download-Options header. Controls how browsers handle file downloads. diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index abaa7fa6..966d250f 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -112,7 +112,6 @@ helmet& helmet::operator=(helmet&& other) noexcept return *this; } - helmet:: helmet(helmet&& other) noexcept : impl_(other.impl_) @@ -145,74 +144,101 @@ operator()( helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, const urls::url_view& source) { + if (source.empty()) + detail::throw_invalid_argument(); + if (allow.empty()) detail::throw_invalid_argument(); std::string value = allow; - if (source.empty()) - detail::throw_invalid_argument(); - - value.push_back(' '); + value += " "; value += source.data(); - list.emplace_back(value); + auto it = std::find_if(list.begin(), list.end(), [&allow](const std::string& in) { + return in.find(allow) != std::string::npos; + }); + + if (it == list.end()) + { + list.emplace_back(value); + } + else + { + auto& it_elem = *it; + + it_elem += " "; + it_elem += source.data(); + } + return *this; } helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, const csp_type& type) { - if (std::find(list.cbegin(), list.cend(), allow) == list.cend()) - { - std::string value = allow; + if (allow.empty()) + detail::throw_invalid_argument(); + + std::string value; - switch (type) + switch (type) + { + case csp_type::sandbox: + { + value += " 'sandbox'"; + break; + } + case csp_type::none: + { + value += " 'none'"; + break; + } + case csp_type::unsafe_inline: + { + value += " 'unsafe-inline'"; + break; + } + case csp_type::unsafe_eval: + { + value += " 'unsafe-eval'"; + break; + } + case csp_type::strict_dynamic: + { + value += " 'strict-dynamic'"; + break; + } + case csp_type::report_sample: + { + value += " 'report-sample'"; + break; + } + case csp_type::self: { - case csp_type::sandbox: - { - value += " 'sandbox'"; - break; - } - case csp_type::none: - { - value += " 'none'"; - break; - } - case csp_type::unsafe_inline: - { - value += " 'unsafe-inline'"; - break; - } - case csp_type::unsafe_eval: - { - value += " 'unsafe-eval'"; - break; - } - case csp_type::strict_dynamic: - { - value += " 'strict-dynamic'"; - break; - } - case csp_type::report_sample: - { - value += " 'report-sample'"; - break; - } - case csp_type::self: - { - value += " 'self'"; - break; - } - default: - detail::throw_invalid_argument(); - return *this; + value += " 'self'"; + break; } + default: + detail::throw_invalid_argument(); + return *this; + } - list.emplace_back(value); + auto it = std::find_if(list.begin(), list.end(), [&allow](const std::string& in) { + return in.find(allow) != std::string::npos; + }); + + if (it == list.end()) + { + std::string final_result = allow; + final_result += value; + + list.emplace_back(final_result); } else { - detail::throw_bad_alloc(); + auto& it_elem = *it; + it_elem.push_back(' '); + it_elem += value; } return *this; From 31cf2f883ac5987443bf405140af23ee8c2d1ff5 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 10:48:32 +0100 Subject: [PATCH 19/26] feat: helmet: API implementation patches. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 2 -- src/server/helmet.cpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 7352d595..37a36480 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -336,8 +336,6 @@ class helmet @param type The CSP source expression type. @return A reference to this object for chaining. - - @throws std::invalid_argument if the directive already exists. */ BOOST_HTTP_PROTO_DECL csp_policy& append(const core::string_view& allow, diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 966d250f..e8ac65c9 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -144,7 +144,7 @@ operator()( helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, const urls::url_view& source) { - if (source.empty()) + if (!source.scheme().starts_with("http")) detail::throw_invalid_argument(); if (allow.empty()) From 83d59ceb142fa92b97585fb551df1808a91ebcfd Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 12:43:41 +0100 Subject: [PATCH 20/26] fix: helmet/server.cpp: Fix move constructor and memory leak. Signed-off-by: Amlal El Mahrouss --- src/server/helmet.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index e8ac65c9..c985c1bf 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -27,7 +27,7 @@ namespace detail { for (const auto& field : fields_value) { cached_fields += field; - cached_fields += ";"; + cached_fields += "; "; } return cached_fields; @@ -106,6 +106,7 @@ helmet::~helmet() helmet& helmet::operator=(helmet&& other) noexcept { + delete impl_; impl_ = other.impl_; other.impl_ = nullptr; @@ -116,6 +117,8 @@ helmet:: helmet(helmet&& other) noexcept : impl_(other.impl_) { + delete impl_; + impl_ = other.impl_; other.impl_ = nullptr; } From 72951cea77c4874c8af414ad39cd7a85567a0f3b Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 15:10:49 +0100 Subject: [PATCH 21/26] feat: helmet! breaking API changes, segfault fix, and better naming. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 92 +++++++--------------- src/server/helmet.cpp | 24 +----- test/unit/server/helmet.cpp | 8 +- 3 files changed, 35 insertions(+), 89 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 37a36480..b539ea6a 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -43,24 +43,7 @@ struct helmet_options */ helmet_options& set(const pair_type& helmet_hdr); - /** Construct helmet options with secure defaults. - - Initializes the following headers: - - X-Download-Options: noopen - - X-Frame-Options: DENY - - X-XSS-Protection: 0 - - X-Content-Type-Options: nosniff - - Strict-Transport-Security - - Cross-Origin-Opener-Policy - - Cross-Origin-Resource-Policy - - Origin-Agent-Cluster - - Referrer-Policy: no-referrer - - X-DNS-Prefetch-Control: off - - X-Permitted-Cross-Domain-Policies: none - - Removes X-Powered-By header - */ helmet_options(); - ~helmet_options(); }; @@ -221,23 +204,12 @@ enum class cross_domain_policy_type all }; -/** HTTP Strict Transport Security options. - - Configures the Strict-Transport-Security header. - - @see strict_transport_security -*/ -struct hsts_options -{ - /** Maximum age in seconds (default: 1 year) */ - std::size_t max_age = 31536000; - - /** Apply to all subdomains */ - bool include_sub_domains = true; - - /** Request preload list inclusion */ - bool preload = false; -}; +/** Groups the hsts constants here. */ +namespace hsts { + inline static constexpr bool preload = true; + inline static constexpr bool include_subdomains = true; + inline static constexpr size_t default_age = 31536000; +} /** List of allowed sources for CSP directives */ using csp_allow_list = std::vector; @@ -258,7 +230,7 @@ using option_pair = std::pair>; opts.set(x_frame_origin(helmet_origin_type::deny)); helmet::csp_policy csp; - csp.append("script-src", csp_type::self); + csp.set("script-src", csp_type::self); opts.set(content_security_policy(csp)); srv.wwwroot.use(helmet(opts)); @@ -269,19 +241,9 @@ using option_pair = std::pair>; class helmet { struct impl; - impl* impl_; + impl* impl_{}; public: - /** Construct helmet middleware with specified options. - - The constructor pre-computes and caches all header - values for efficient application to responses. - - @param options Security header configuration. If not - provided, secure defaults are used. - - @see helmet_options - */ BOOST_HTTP_PROTO_DECL explicit helmet( helmet_options options = {}); @@ -292,16 +254,6 @@ class helmet helmet& operator=(helmet&&) noexcept; helmet(helmet&&) noexcept; - /** Apply security headers to the response. - - Iterates through all configured headers and applies them - to the response. Empty header values indicate headers - that should be removed (e.g., X-Powered-By). - - @param p Route parameters containing the response to modify. - - @return Always returns route::next to continue processing. - */ BOOST_HTTP_PROTO_DECL route_result operator()(route_params& p) const; @@ -314,9 +266,9 @@ class helmet @par Example @code helmet::csp_policy policy; - policy.append("script-src", csp_type::self) - .append("style-src", "https://example.com") - .append("img-src", csp_type::none); + policy.set("script-src", csp_type::self) + .set("style-src", "https://example.com") + .set("img-src", csp_type::none); @endcode */ struct csp_policy @@ -338,7 +290,7 @@ class helmet @return A reference to this object for chaining. */ BOOST_HTTP_PROTO_DECL - csp_policy& append(const core::string_view& allow, + csp_policy& set(const core::string_view& allow, const csp_type& type); /** Remove a CSP directive. @@ -362,7 +314,7 @@ class helmet @throws std::invalid_argument if parameters are empty. */ BOOST_HTTP_PROTO_DECL - csp_policy& append(const core::string_view& allow, + csp_policy& set(const core::string_view& allow, const urls::url_view& source); }; }; @@ -424,7 +376,23 @@ option_pair content_security_policy(const helmet::csp_policy& sp); @return Header name and value pair. */ -option_pair strict_transport_security(const hsts_options& options = hsts_options{}); +template +inline option_pair strict_transport_security() +{ + std::string value = "max-age=" + std::to_string(Age); + + if constexpr (IncludeDomains) + { + value += "; includeSubDomains"; + } + + if constexpr (Preload) + { + value += "; preload"; + } + + return {"Strict-Transport-Security", {value}}; +} /** Return Cross-Origin-Opener-Policy header configuration. diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index c985c1bf..edf2cf81 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -42,7 +42,7 @@ helmet_options::helmet_options() this->set(x_frame_origin(helmet_origin_type::deny)); this->set(x_xss_protection()); this->set(x_content_type_options()); - this->set(strict_transport_security()); + this->set(strict_transport_security()); this->set(cross_origin_opener_policy()); this->set(cross_origin_resource_policy()); this->set(origin_agent_cluster()); @@ -115,7 +115,6 @@ helmet& helmet::operator=(helmet&& other) noexcept helmet:: helmet(helmet&& other) noexcept - : impl_(other.impl_) { delete impl_; impl_ = other.impl_; @@ -144,7 +143,7 @@ operator()( return route::next; } -helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, +helmet::csp_policy& helmet::csp_policy::set(const core::string_view& allow, const urls::url_view& source) { if (!source.scheme().starts_with("http")) @@ -177,7 +176,7 @@ helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, return *this; } -helmet::csp_policy& helmet::csp_policy::append(const core::string_view& allow, const csp_type& type) +helmet::csp_policy& helmet::csp_policy::set(const core::string_view& allow, const csp_type& type) { if (allow.empty()) detail::throw_invalid_argument(); @@ -308,23 +307,6 @@ option_pair x_download_options(const helmet_download_type& type) return {}; } -option_pair strict_transport_security(const hsts_options& options) -{ - std::string value = "max-age=" + std::to_string(options.max_age); - - if (options.include_sub_domains) - { - value += "; includeSubDomains"; - } - - if (options.preload) - { - value += "; preload"; - } - - return {"Strict-Transport-Security", {value}}; -} - option_pair cross_origin_opener_policy(const coop_policy_type& policy) { std::string value; diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp index fc82b8c9..4cf924c2 100644 --- a/test/unit/server/helmet.cpp +++ b/test/unit/server/helmet.cpp @@ -97,7 +97,7 @@ struct helmet_test { helmet_options opt; - opt.set(strict_transport_security()); + opt.set(strict_transport_security()); helmet helmet{opt}; route_params p; @@ -111,12 +111,8 @@ struct helmet_test // Test Strict-Transport-Security with custom options { helmet_options opt; - hsts_options hsts; - hsts.max_age = 86400; - hsts.include_sub_domains = true; - hsts.preload = true; - opt.set(strict_transport_security(hsts)); + opt.set(strict_transport_security<86400, hsts::include_subdomains, hsts::preload>()); helmet helmet{opt}; route_params p; From 8e0e20bda4b26b0eddb502aafc7960d7e5cb2f9c Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 15:25:38 +0100 Subject: [PATCH 22/26] feat: helmet: add allow methods, drop experiment with hsts. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 130 +-------------------- src/server/helmet.cpp | 23 +++- test/unit/server/helmet.cpp | 4 +- 3 files changed, 25 insertions(+), 132 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index b539ea6a..5aa71347 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -290,7 +290,7 @@ class helmet @return A reference to this object for chaining. */ BOOST_HTTP_PROTO_DECL - csp_policy& set(const core::string_view& allow, + csp_policy& allow(const core::string_view& allow, const csp_type& type); /** Remove a CSP directive. @@ -314,161 +314,37 @@ class helmet @throws std::invalid_argument if parameters are empty. */ BOOST_HTTP_PROTO_DECL - csp_policy& set(const core::string_view& allow, + csp_policy& allow(const core::string_view& allow, const urls::url_view& source); }; }; -/** Return X-Download-Options header configuration. - - Controls file download behavior in Internet Explorer 8+. - - @param type The download restriction type. - - @return Header name and value pair. -*/ option_pair x_download_options(const helmet_download_type& type); -/** Return X-Frame-Options header configuration. - - Prevents clickjacking attacks by controlling frame embedding. - - @param origin The frame embedding policy. - - @return Header name and value pair. -*/ option_pair x_frame_origin(const helmet_origin_type& origin); -/** Return X-XSS-Protection header configuration. - - Disables legacy XSS filtering which can create vulnerabilities. - Modern browsers rely on CSP instead. - - @return Header name and value pair (always "0"). -*/ option_pair x_xss_protection(); -/** Return X-Content-Type-Options header configuration. - - Prevents MIME-type sniffing attacks. - - @return Header name and value pair (always "nosniff"). -*/ option_pair x_content_type_options(); -/** Return Content-Security-Policy header configuration. - - Configures CSP to prevent XSS and data injection attacks. - - @param sp The CSP policy with directives. - - @return Header name and value pair. - - @throws std::invalid_argument if the policy is empty. -*/ option_pair content_security_policy(const helmet::csp_policy& sp); -/** Return Strict-Transport-Security header configuration. - - Enforces HTTPS connections to prevent downgrade attacks. - - @param options HSTS configuration parameters. - - @return Header name and value pair. -*/ -template -inline option_pair strict_transport_security() -{ - std::string value = "max-age=" + std::to_string(Age); - - if constexpr (IncludeDomains) - { - value += "; includeSubDomains"; - } - - if constexpr (Preload) - { - value += "; preload"; - } - - return {"Strict-Transport-Security", {value}}; -} - -/** Return Cross-Origin-Opener-Policy header configuration. - - Controls cross-origin window isolation. - - @param policy The COOP policy type. +option_pair strict_transport_security(const std::size_t age, const bool include_domains = true, const bool preload = false); - @return Header name and value pair. -*/ option_pair cross_origin_opener_policy(const coop_policy_type& policy = coop_policy_type::same_origin); -/** Return Cross-Origin-Resource-Policy header configuration. - - Controls cross-origin resource sharing. - - @param policy The CORP policy type. - - @return Header name and value pair. -*/ option_pair cross_origin_resource_policy(const corp_policy_type& policy = corp_policy_type::same_origin); -/** Return Cross-Origin-Embedder-Policy header configuration. - - Controls embedding of cross-origin resources. - - @param policy The COEP policy type. - - @return Header name and value pair. -*/ option_pair cross_origin_embedder_policy(const coep_policy_type& policy = coep_policy_type::require_corp); -/** Return Referrer-Policy header configuration. - - Controls how much referrer information is sent with requests. - - @param policy The referrer policy type. - - @return Header name and value pair. -*/ option_pair referrer_policy(const referrer_policy_type& policy = referrer_policy_type::no_referrer); -/** Return Origin-Agent-Cluster header configuration. - - Requests origin-keyed agent clusters for better isolation. - - @return Header name and value pair (always "?1"). -*/ option_pair origin_agent_cluster(); -/** Return X-DNS-Prefetch-Control header configuration. - - Controls DNS prefetching to balance performance and privacy. - - @param allow Whether to enable DNS prefetching. - - @return Header name and value pair. -*/ option_pair dns_prefetch_control(bool allow = false); -/** Return X-Permitted-Cross-Domain-Policies header configuration. - - Controls cross-domain policy files for Flash and Adobe Acrobat. - - @param policy The cross-domain policy type. - - @return Header name and value pair. -*/ option_pair permitted_cross_domain_policies(const cross_domain_policy_type& policy = cross_domain_policy_type::none); -/** Return configuration to remove X-Powered-By header. - - Removes the X-Powered-By header to avoid revealing - server technology details. - - @return Header name with empty value (triggers removal). -*/ option_pair hide_powered_by(); } diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index edf2cf81..c63a49ab 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -42,7 +42,7 @@ helmet_options::helmet_options() this->set(x_frame_origin(helmet_origin_type::deny)); this->set(x_xss_protection()); this->set(x_content_type_options()); - this->set(strict_transport_security()); + this->set(strict_transport_security(hsts::default_age, hsts::include_subdomains, hsts::preload)); this->set(cross_origin_opener_policy()); this->set(cross_origin_resource_policy()); this->set(origin_agent_cluster()); @@ -143,7 +143,7 @@ operator()( return route::next; } -helmet::csp_policy& helmet::csp_policy::set(const core::string_view& allow, +helmet::csp_policy& helmet::csp_policy::allow(const core::string_view& allow, const urls::url_view& source) { if (!source.scheme().starts_with("http")) @@ -176,7 +176,7 @@ helmet::csp_policy& helmet::csp_policy::set(const core::string_view& allow, return *this; } -helmet::csp_policy& helmet::csp_policy::set(const core::string_view& allow, const csp_type& type) +helmet::csp_policy& helmet::csp_policy::allow(const core::string_view& allow, const csp_type& type) { if (allow.empty()) detail::throw_invalid_argument(); @@ -373,6 +373,23 @@ option_pair cross_origin_embedder_policy(const coep_policy_type& policy) return {"Cross-Origin-Embedder-Policy", {value}}; } +option_pair strict_transport_security(const std::size_t age, const bool include_domains, const bool preload) +{ + std::string value = "max-age=" + std::to_string(age); + + if (include_domains) + { + value += "; includeSubDomains"; + } + + if (preload) + { + value += "; preload"; + } + + return {"Strict-Transport-Security", {value}}; +} + option_pair referrer_policy(const referrer_policy_type& policy) { std::string value; diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp index 4cf924c2..ea8439c5 100644 --- a/test/unit/server/helmet.cpp +++ b/test/unit/server/helmet.cpp @@ -97,7 +97,7 @@ struct helmet_test { helmet_options opt; - opt.set(strict_transport_security()); + opt.set(strict_transport_security(hsts::default_age)); helmet helmet{opt}; route_params p; @@ -112,7 +112,7 @@ struct helmet_test { helmet_options opt; - opt.set(strict_transport_security<86400, hsts::include_subdomains, hsts::preload>()); + opt.set(strict_transport_security(86400, false, false)); helmet helmet{opt}; route_params p; From 10c0cb534bccdf2f8f504c92fccba5013ce5bb23 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 15:35:31 +0100 Subject: [PATCH 23/26] feat: better helmet implementation of hsts. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 12 +++++++++++- src/server/helmet.cpp | 2 +- test/unit/server/helmet.cpp | 6 +++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 5aa71347..71f9f858 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -207,6 +207,8 @@ enum class cross_domain_policy_type /** Groups the hsts constants here. */ namespace hsts { inline static constexpr bool preload = true; + inline static constexpr bool no_preload = false; + inline static constexpr bool no_subdomains = false; inline static constexpr bool include_subdomains = true; inline static constexpr size_t default_age = 31536000; } @@ -329,7 +331,15 @@ option_pair x_content_type_options(); option_pair content_security_policy(const helmet::csp_policy& sp); -option_pair strict_transport_security(const std::size_t age, const bool include_domains = true, const bool preload = false); +/** Return HSTS configuration for the host. + @param include_subdomains either include_domains or no_subdomains + @param preload either preload or no_preload + @note use the hsts namespace to set those function values. + @return the option_pair to pass to the helmet. +*/ +option_pair strict_transport_security(std::size_t age, + bool include_subdomains = hsts::include_subdomains, + bool preload = hsts::no_preload); option_pair cross_origin_opener_policy(const coop_policy_type& policy = coop_policy_type::same_origin); diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index c63a49ab..be9a1650 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -373,7 +373,7 @@ option_pair cross_origin_embedder_policy(const coep_policy_type& policy) return {"Cross-Origin-Embedder-Policy", {value}}; } -option_pair strict_transport_security(const std::size_t age, const bool include_domains, const bool preload) +option_pair strict_transport_security(std::size_t age, bool include_domains, bool preload) { std::string value = "max-age=" + std::to_string(age); diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp index ea8439c5..530e4907 100644 --- a/test/unit/server/helmet.cpp +++ b/test/unit/server/helmet.cpp @@ -252,9 +252,9 @@ struct helmet_test helmet_options opt; helmet::csp_policy csp; - csp.append("default-src", csp_type::self); - csp.append("script-src", csp_type::self); - csp.append("style-src", csp_type::self); + csp.allow("default-src", csp_type::self) + .allow("script-src", csp_type::self) + .allow("style-src", csp_type::self); opt.set(content_security_policy(csp)); From 1f9865ae908b570dd13880569ac2f9f362673a6e Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 15:41:17 +0100 Subject: [PATCH 24/26] feat: update helmet documentation for strict_transport_security. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 71f9f858..172af5ee 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -332,12 +332,14 @@ option_pair x_content_type_options(); option_pair content_security_policy(const helmet::csp_policy& sp); /** Return HSTS configuration for the host. - @param include_subdomains either include_domains or no_subdomains - @param preload either preload or no_preload - @note use the hsts namespace to set those function values. - @return the option_pair to pass to the helmet. + @param age Maximum age in seconds for HSTS enforcement. + @param include_subdomains either hsts::include_subdomains or hsts::no_subdomains + @param preload either hsts::preload or hsts::no_preload + @note Use constants from the hsts namespace. + @return Header name and value pair. */ -option_pair strict_transport_security(std::size_t age, +option_pair strict_transport_security( + std::size_t age, bool include_subdomains = hsts::include_subdomains, bool preload = hsts::no_preload); From ef40e62b2598c4a5948abdaf633a6750c15603a6 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 16:31:55 +0100 Subject: [PATCH 25/26] fix: hemlet.cpp: fix refactors in helmet. Signed-off-by: Amlal El Mahrouss --- src/server/helmet.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index be9a1650..40b68e35 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -7,8 +7,6 @@ // Official repository: https://github.com/cppalliance/http_proto // -#define BOOST_HTTP_PROTO_HELMET_IMPL 1 - #include #include From 24e374b02457b90b3c31e7af9818623d561de9c7 Mon Sep 17 00:00:00 2001 From: Amlal El Mahrouss Date: Thu, 25 Dec 2025 17:30:40 +0100 Subject: [PATCH 26/26] fix: helmet.cpp: documentation and move constructor fixes. Signed-off-by: Amlal El Mahrouss --- include/boost/http_proto/server/helmet.hpp | 10 +++++----- src/server/helmet.cpp | 12 +++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp index 172af5ee..9861a616 100644 --- a/include/boost/http_proto/server/helmet.hpp +++ b/include/boost/http_proto/server/helmet.hpp @@ -232,7 +232,7 @@ using option_pair = std::pair>; opts.set(x_frame_origin(helmet_origin_type::deny)); helmet::csp_policy csp; - csp.set("script-src", csp_type::self); + csp.allow("script-src", csp_type::self); opts.set(content_security_policy(csp)); srv.wwwroot.use(helmet(opts)); @@ -268,9 +268,9 @@ class helmet @par Example @code helmet::csp_policy policy; - policy.set("script-src", csp_type::self) - .set("style-src", "https://example.com") - .set("img-src", csp_type::none); + policy.allow("script-src", csp_type::self) + .allow("style-src", "https://example.com") + .allow("img-src", csp_type::none); @endcode */ struct csp_policy @@ -284,7 +284,7 @@ class helmet BOOST_HTTP_PROTO_DECL ~csp_policy(); - /** Append a CSP directive with a predefined type. + /** Allows a CSP directive with a predefined type. @param allow The directive name (e.g., "script-src"). @param type The CSP source expression type. diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp index 40b68e35..8080d65d 100644 --- a/src/server/helmet.cpp +++ b/src/server/helmet.cpp @@ -113,9 +113,8 @@ helmet& helmet::operator=(helmet&& other) noexcept helmet:: helmet(helmet&& other) noexcept + : impl_(other.impl_) { - delete impl_; - impl_ = other.impl_; other.impl_ = nullptr; } @@ -156,7 +155,8 @@ helmet::csp_policy& helmet::csp_policy::allow(const core::string_view& allow, value += source.data(); auto it = std::find_if(list.begin(), list.end(), [&allow](const std::string& in) { - return in.find(allow) != std::string::npos; + std::string allow_with_space = std::string(allow) + " "; + return in.find(allow_with_space) != std::string::npos; }); if (it == list.end()) @@ -224,7 +224,8 @@ helmet::csp_policy& helmet::csp_policy::allow(const core::string_view& allow, co } auto it = std::find_if(list.begin(), list.end(), [&allow](const std::string& in) { - return in.find(allow) != std::string::npos; + std::string allow_with_space = std::string(allow) + " "; + return in.find(allow_with_space) != std::string::npos; }); if (it == list.end()) @@ -250,7 +251,8 @@ helmet::csp_policy::~csp_policy() = default; helmet::csp_policy& helmet::csp_policy::remove(const core::string_view& allow) { if (auto it = std::find_if(list.cbegin(), list.cend(), [&allow](const std::string& in) { - return in.find(allow) != std::string::npos; + std::string allow_with_space = std::string(allow) + " "; + return in.find(allow_with_space) != std::string::npos; }); it != list.cend()) { list.erase(it);