From c518ac1a32d47e6c7c22952801e64b401905e47a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:50:06 +0000 Subject: [PATCH 1/2] Initial plan From 5950c758b0134aaf3063d564d70f09354d0b1a25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:56:44 +0000 Subject: [PATCH 2/2] Add support for Cross-Origin headers (COOP, CORP, COEP) Co-authored-by: dvershinin <250071+dvershinin@users.noreply.github.com> --- README.md | 30 ++++ src/ngx_http_security_headers_module.c | 161 ++++++++++++++++++++ t/headers.t | 198 ++++++++++++++++++++++++- 3 files changed, 388 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f0207b..ea3cf9d 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,36 @@ Special `omit` value will disable sending the header by the module. Controls inclusion and value of [`Referrer-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) header. Special `omit` value will disable sending the header by the module. +### `security_headers_coop` + +- **syntax**: `security_headers_coop unsafe-none | same-origin-allow-popups | same-origin | omit` +- **default**: `omit` +- **context**: `http`, `server`, `location` + +Controls inclusion and value of [`Cross-Origin-Opener-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) header. +This header allows you to ensure a top-level document does not share a browsing context group with cross-origin documents. +Special `omit` value (default) will disable sending the header by the module. + +### `security_headers_corp` + +- **syntax**: `security_headers_corp same-site | same-origin | cross-origin | omit` +- **default**: `omit` +- **context**: `http`, `server`, `location` + +Controls inclusion and value of [`Cross-Origin-Resource-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy) header. +This header conveys a desire that the browser blocks no-cors cross-origin/cross-site requests to the given resource. +Special `omit` value (default) will disable sending the header by the module. + +### `security_headers_coep` + +- **syntax**: `security_headers_coep unsafe-none | require-corp | omit` +- **default**: `omit` +- **context**: `http`, `server`, `location` + +Controls inclusion and value of [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) header. +This header prevents a document from loading any cross-origin resources that don't explicitly grant the document permission. +Special `omit` value (default) will disable sending the header by the module. + ## Install We highly recommend installing using packages, where available, diff --git a/src/ngx_http_security_headers_module.c b/src/ngx_http_security_headers_module.c index 1ba000c..56c1ea4 100644 --- a/src/ngx_http_security_headers_module.c +++ b/src/ngx_http_security_headers_module.c @@ -27,6 +27,20 @@ #define NGX_HTTP_RP_HEADER_STRICT_ORIG_WHEN_CROSS 7 #define NGX_HTTP_RP_HEADER_UNSAFE_URL 8 +/* Cross-Origin-Opener-Policy header */ +#define NGX_HTTP_COOP_HEADER_UNSAFE_NONE 1 +#define NGX_HTTP_COOP_HEADER_SAME_ORIGIN_POPUPS 2 +#define NGX_HTTP_COOP_HEADER_SAME_ORIGIN 3 + +/* Cross-Origin-Resource-Policy header */ +#define NGX_HTTP_CORP_HEADER_SAME_SITE 1 +#define NGX_HTTP_CORP_HEADER_SAME_ORIGIN 2 +#define NGX_HTTP_CORP_HEADER_CROSS_ORIGIN 3 + +/* Cross-Origin-Embedder-Policy header */ +#define NGX_HTTP_COEP_HEADER_UNSAFE_NONE 1 +#define NGX_HTTP_COEP_HEADER_REQUIRE_CORP 2 + typedef struct { ngx_flag_t enable; ngx_flag_t hide_server_tokens; @@ -35,6 +49,9 @@ typedef struct { ngx_uint_t xss; ngx_uint_t fo; ngx_uint_t rp; + ngx_uint_t coop; + ngx_uint_t corp; + ngx_uint_t coep; ngx_hash_t text_types; ngx_array_t *text_types_keys; @@ -113,6 +130,51 @@ static ngx_conf_enum_t ngx_http_referrer_policy[] = { { ngx_null_string, 0 } }; +static ngx_conf_enum_t ngx_http_cross_origin_opener_policy[] = { + { ngx_string("unsafe-none"), + NGX_HTTP_COOP_HEADER_UNSAFE_NONE }, + + { ngx_string("same-origin-allow-popups"), + NGX_HTTP_COOP_HEADER_SAME_ORIGIN_POPUPS }, + + { ngx_string("same-origin"), + NGX_HTTP_COOP_HEADER_SAME_ORIGIN }, + + { ngx_string("omit"), + NGX_HTTP_SECURITY_HEADER_OMIT }, + + { ngx_null_string, 0 } +}; + +static ngx_conf_enum_t ngx_http_cross_origin_resource_policy[] = { + { ngx_string("same-site"), + NGX_HTTP_CORP_HEADER_SAME_SITE }, + + { ngx_string("same-origin"), + NGX_HTTP_CORP_HEADER_SAME_ORIGIN }, + + { ngx_string("cross-origin"), + NGX_HTTP_CORP_HEADER_CROSS_ORIGIN }, + + { ngx_string("omit"), + NGX_HTTP_SECURITY_HEADER_OMIT }, + + { ngx_null_string, 0 } +}; + +static ngx_conf_enum_t ngx_http_cross_origin_embedder_policy[] = { + { ngx_string("unsafe-none"), + NGX_HTTP_COEP_HEADER_UNSAFE_NONE }, + + { ngx_string("require-corp"), + NGX_HTTP_COEP_HEADER_REQUIRE_CORP }, + + { ngx_string("omit"), + NGX_HTTP_SECURITY_HEADER_OMIT }, + + { ngx_null_string, 0 } +}; + static ngx_int_t ngx_http_security_headers_filter(ngx_http_request_t *r); static void *ngx_http_security_headers_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf, @@ -173,6 +235,27 @@ static ngx_command_t ngx_http_security_headers_commands[] = { offsetof(ngx_http_security_headers_loc_conf_t, rp), ngx_http_referrer_policy }, + { ngx_string("security_headers_coop"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_security_headers_loc_conf_t, coop), + ngx_http_cross_origin_opener_policy }, + + { ngx_string("security_headers_corp"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_security_headers_loc_conf_t, corp), + ngx_http_cross_origin_resource_policy }, + + { ngx_string("security_headers_coep"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_security_headers_loc_conf_t, coep), + ngx_http_cross_origin_embedder_policy }, + { ngx_string("security_headers_text_types"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, ngx_http_types_slot, @@ -372,6 +455,75 @@ ngx_http_security_headers_filter(ngx_http_request_t *r) } } + /* Cross-Origin-Opener-Policy */ + if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED + && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->coop) { + + switch (slcf->coop) { + case NGX_HTTP_COOP_HEADER_UNSAFE_NONE: + ngx_str_set(&val, "unsafe-none"); + break; + case NGX_HTTP_COOP_HEADER_SAME_ORIGIN_POPUPS: + ngx_str_set(&val, "same-origin-allow-popups"); + break; + case NGX_HTTP_COOP_HEADER_SAME_ORIGIN: + ngx_str_set(&val, "same-origin"); + break; + default: + val.len = 0; + val.data = NULL; + } + if (val.data) { + ngx_str_set(&key, "Cross-Origin-Opener-Policy"); + ngx_set_headers_out_by_search(r, &key, &val); + } + } + + /* Cross-Origin-Resource-Policy */ + if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED + && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->corp) { + + switch (slcf->corp) { + case NGX_HTTP_CORP_HEADER_SAME_SITE: + ngx_str_set(&val, "same-site"); + break; + case NGX_HTTP_CORP_HEADER_SAME_ORIGIN: + ngx_str_set(&val, "same-origin"); + break; + case NGX_HTTP_CORP_HEADER_CROSS_ORIGIN: + ngx_str_set(&val, "cross-origin"); + break; + default: + val.len = 0; + val.data = NULL; + } + if (val.data) { + ngx_str_set(&key, "Cross-Origin-Resource-Policy"); + ngx_set_headers_out_by_search(r, &key, &val); + } + } + + /* Cross-Origin-Embedder-Policy */ + if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED + && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->coep) { + + switch (slcf->coep) { + case NGX_HTTP_COEP_HEADER_UNSAFE_NONE: + ngx_str_set(&val, "unsafe-none"); + break; + case NGX_HTTP_COEP_HEADER_REQUIRE_CORP: + ngx_str_set(&val, "require-corp"); + break; + default: + val.len = 0; + val.data = NULL; + } + if (val.data) { + ngx_str_set(&key, "Cross-Origin-Embedder-Policy"); + ngx_set_headers_out_by_search(r, &key, &val); + } + } + /* proceed to the next handler in chain */ @@ -392,6 +544,9 @@ ngx_http_security_headers_create_loc_conf(ngx_conf_t *cf) conf->xss = NGX_CONF_UNSET_UINT; conf->fo = NGX_CONF_UNSET_UINT; conf->rp = NGX_CONF_UNSET_UINT; + conf->coop = NGX_CONF_UNSET_UINT; + conf->corp = NGX_CONF_UNSET_UINT; + conf->coep = NGX_CONF_UNSET_UINT; conf->enable = NGX_CONF_UNSET; conf->hide_server_tokens = NGX_CONF_UNSET_UINT; conf->hsts_preload = NGX_CONF_UNSET_UINT; @@ -425,6 +580,12 @@ ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf, void *parent, NGX_HTTP_FO_HEADER_SAME); ngx_conf_merge_uint_value(conf->rp, prev->rp, NGX_HTTP_RP_HEADER_STRICT_ORIG_WHEN_CROSS); + ngx_conf_merge_uint_value(conf->coop, prev->coop, + NGX_HTTP_SECURITY_HEADER_OMIT); + ngx_conf_merge_uint_value(conf->corp, prev->corp, + NGX_HTTP_SECURITY_HEADER_OMIT); + ngx_conf_merge_uint_value(conf->coep, prev->coep, + NGX_HTTP_SECURITY_HEADER_OMIT); return NGX_CONF_OK; } diff --git a/t/headers.t b/t/headers.t index 939010a..d936a9e 100644 --- a/t/headers.t +++ b/t/headers.t @@ -224,4 +224,200 @@ hello world --- response_headers !server x-powered-by: PHP -x-generator: Drupal \ No newline at end of file +x-generator: Drupal + + + +=== TEST 12: COOP header with same-origin +--- config + security_headers on; + security_headers_coop same-origin; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-opener-policy: same-origin + + + +=== TEST 13: COOP header with same-origin-allow-popups +--- config + security_headers on; + security_headers_coop same-origin-allow-popups; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-opener-policy: same-origin-allow-popups + + + +=== TEST 14: COOP header with unsafe-none +--- config + security_headers on; + security_headers_coop unsafe-none; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-opener-policy: unsafe-none + + + +=== TEST 15: COOP header omitted +--- config + security_headers on; + security_headers_coop omit; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +!cross-origin-opener-policy + + + +=== TEST 16: CORP header with same-origin +--- config + security_headers on; + security_headers_corp same-origin; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-resource-policy: same-origin + + + +=== TEST 17: CORP header with same-site +--- config + security_headers on; + security_headers_corp same-site; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-resource-policy: same-site + + + +=== TEST 18: CORP header with cross-origin +--- config + security_headers on; + security_headers_corp cross-origin; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-resource-policy: cross-origin + + + +=== TEST 19: CORP header omitted +--- config + security_headers on; + security_headers_corp omit; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +!cross-origin-resource-policy + + + +=== TEST 20: COEP header with require-corp +--- config + security_headers on; + security_headers_coep require-corp; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-embedder-policy: require-corp + + + +=== TEST 21: COEP header with unsafe-none +--- config + security_headers on; + security_headers_coep unsafe-none; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-embedder-policy: unsafe-none + + + +=== TEST 22: COEP header omitted +--- config + security_headers on; + security_headers_coep omit; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +!cross-origin-embedder-policy + + + +=== TEST 23: All cross-origin headers together +--- config + security_headers on; + security_headers_coop same-origin; + security_headers_corp same-origin; + security_headers_coep require-corp; + location = /hello { + return 200 "hello world\n"; + } +--- request + GET /hello +--- response_body +hello world +--- response_headers +cross-origin-opener-policy: same-origin +cross-origin-resource-policy: same-origin +cross-origin-embedder-policy: require-corp \ No newline at end of file