Skip to content

Commit 62d2685

Browse files
committed
Add Cross-Origin security headers (CORP, COOP, COEP)
Implements support for three Cross-Origin HTTP security headers: - Cross-Origin-Resource-Policy (CORP) via security_headers_corp Default: same-site (safe opt-out default) Values: same-site, same-origin, cross-origin, omit - Cross-Origin-Opener-Policy (COOP) via security_headers_coop Default: omit (opt-in to avoid breaking sites) Values: same-origin, same-origin-allow-popups, unsafe-none, omit - Cross-Origin-Embedder-Policy (COEP) via security_headers_coep Default: omit (opt-in to avoid breaking sites) Values: require-corp, credentialless, unsafe-none, omit The credentialless value for COEP provides a middle ground for cross-origin isolation without requiring all resources to have CORP. Fixes #17
1 parent 1eef9a4 commit 62d2685

4 files changed

Lines changed: 419 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [0.2.0] - 2026-02-03
5+
### Added
6+
* Cross-Origin-Resource-Policy (CORP) header support via `security_headers_corp` directive (default: `same-site`)
7+
* Cross-Origin-Opener-Policy (COOP) header support via `security_headers_coop` directive (default: `omit`)
8+
* Cross-Origin-Embedder-Policy (COEP) header support via `security_headers_coep` directive (default: `omit`)
9+
* COEP `credentialless` value for more flexible cross-origin isolation
10+
11+
Fixes #17
12+
413
## [0.1.0] - 2023-09-06
514
#### Fixed
615
* HSTS set to 1 year instead of 2 years by default (#18)

README.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Connection: keep-alive
2727
X-Content-Type-Options: nosniff
2828
X-XSS-Protection: 0
2929
Referrer-Policy: strict-origin-when-cross-origin
30+
Cross-Origin-Resource-Policy: same-site
3031
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload</b>
3132
</pre>
3233

@@ -73,6 +74,7 @@ Enables or disables applying security headers. The default set includes:
7374
* `X-XSS-Protection: 0`
7475
* `Referrer-Policy: strict-origin-when-cross-origin`
7576
* `X-Content-Type-Options: nosniff`
77+
* `Cross-Origin-Resource-Policy: same-site`
7678

7779
The values of these headers (or their inclusion) can be controlled with other `security_headers_*` directives below.
7880

@@ -132,8 +134,60 @@ Special `omit` value will disable sending the header by the module.
132134
- **default**: `strict-origin-when-cross-origin`
133135
- **context**: `http`, `server`, `location`
134136

135-
Controls inclusion and value of [`Referrer-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) header.
136-
Special `omit` value will disable sending the header by the module.
137+
Controls inclusion and value of [`Referrer-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) header.
138+
Special `omit` value will disable sending the header by the module.
139+
140+
### `security_headers_corp`
141+
142+
- **syntax**: `security_headers_corp same-site | same-origin | cross-origin | omit`
143+
- **default**: `same-site`
144+
- **context**: `http`, `server`, `location`
145+
146+
Controls inclusion and value of [`Cross-Origin-Resource-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy) header.
147+
This header controls how your resources can be embedded by other origins.
148+
Special `omit` value will disable sending the header by the module.
149+
150+
The default `same-site` is a safe choice that prevents cross-site embedding while allowing same-site requests.
151+
152+
### `security_headers_coop`
153+
154+
- **syntax**: `security_headers_coop same-origin | same-origin-allow-popups | unsafe-none | omit`
155+
- **default**: `omit`
156+
- **context**: `http`, `server`, `location`
157+
158+
Controls inclusion and value of [`Cross-Origin-Opener-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) header.
159+
This header controls window opener relationships across origins.
160+
Special `omit` value will disable sending the header by the module.
161+
162+
The default is `omit` because enabling this header can break popup/window.opener communication patterns.
163+
Enable explicitly only if you understand the implications.
164+
165+
### `security_headers_coep`
166+
167+
- **syntax**: `security_headers_coep require-corp | credentialless | unsafe-none | omit`
168+
- **default**: `omit`
169+
- **context**: `http`, `server`, `location`
170+
171+
Controls inclusion and value of [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) header.
172+
This header controls embedding of cross-origin resources.
173+
Special `omit` value will disable sending the header by the module.
174+
175+
The default is `omit` because enabling this header can break sites that load third-party resources
176+
(analytics, CDN assets, ads) without proper CORS headers.
177+
178+
### Cross-Origin Isolation
179+
180+
To enable [cross-origin isolation](https://web.dev/cross-origin-isolation-guide/) (required for `SharedArrayBuffer` and high-resolution timers),
181+
configure all three cross-origin headers:
182+
183+
```nginx
184+
security_headers on;
185+
security_headers_corp same-origin;
186+
security_headers_coop same-origin;
187+
security_headers_coep require-corp;
188+
```
189+
190+
**Warning**: This configuration will break loading of any cross-origin resources that don't explicitly allow it via CORS.
137191

138192
## Install
139193

src/ngx_http_security_headers_module.c

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@
2727
#define NGX_HTTP_RP_HEADER_STRICT_ORIG_WHEN_CROSS 7
2828
#define NGX_HTTP_RP_HEADER_UNSAFE_URL 8
2929

30+
/* Cross-Origin-Resource-Policy */
31+
#define NGX_HTTP_CORP_HEADER_SAME_SITE 1
32+
#define NGX_HTTP_CORP_HEADER_SAME_ORIGIN 2
33+
#define NGX_HTTP_CORP_HEADER_CROSS_ORIGIN 3
34+
35+
/* Cross-Origin-Opener-Policy */
36+
#define NGX_HTTP_COOP_HEADER_SAME_ORIGIN 1
37+
#define NGX_HTTP_COOP_HEADER_SAME_ORIGIN_ALLOW_POPUPS 2
38+
#define NGX_HTTP_COOP_HEADER_UNSAFE_NONE 3
39+
40+
/* Cross-Origin-Embedder-Policy */
41+
#define NGX_HTTP_COEP_HEADER_REQUIRE_CORP 1
42+
#define NGX_HTTP_COEP_HEADER_CREDENTIALLESS 2
43+
#define NGX_HTTP_COEP_HEADER_UNSAFE_NONE 3
44+
3045
typedef struct {
3146
ngx_flag_t enable;
3247
ngx_flag_t hide_server_tokens;
@@ -35,6 +50,9 @@ typedef struct {
3550
ngx_uint_t xss;
3651
ngx_uint_t fo;
3752
ngx_uint_t rp;
53+
ngx_uint_t corp;
54+
ngx_uint_t coop;
55+
ngx_uint_t coep;
3856

3957
ngx_hash_t text_types;
4058
ngx_array_t *text_types_keys;
@@ -113,6 +131,30 @@ static ngx_conf_enum_t ngx_http_referrer_policy[] = {
113131
{ ngx_null_string, 0 }
114132
};
115133

134+
static ngx_conf_enum_t ngx_http_corp[] = {
135+
{ ngx_string("same-site"), NGX_HTTP_CORP_HEADER_SAME_SITE },
136+
{ ngx_string("same-origin"), NGX_HTTP_CORP_HEADER_SAME_ORIGIN },
137+
{ ngx_string("cross-origin"), NGX_HTTP_CORP_HEADER_CROSS_ORIGIN },
138+
{ ngx_string("omit"), NGX_HTTP_SECURITY_HEADER_OMIT },
139+
{ ngx_null_string, 0 }
140+
};
141+
142+
static ngx_conf_enum_t ngx_http_coop[] = {
143+
{ ngx_string("same-origin"), NGX_HTTP_COOP_HEADER_SAME_ORIGIN },
144+
{ ngx_string("same-origin-allow-popups"), NGX_HTTP_COOP_HEADER_SAME_ORIGIN_ALLOW_POPUPS },
145+
{ ngx_string("unsafe-none"), NGX_HTTP_COOP_HEADER_UNSAFE_NONE },
146+
{ ngx_string("omit"), NGX_HTTP_SECURITY_HEADER_OMIT },
147+
{ ngx_null_string, 0 }
148+
};
149+
150+
static ngx_conf_enum_t ngx_http_coep[] = {
151+
{ ngx_string("require-corp"), NGX_HTTP_COEP_HEADER_REQUIRE_CORP },
152+
{ ngx_string("credentialless"), NGX_HTTP_COEP_HEADER_CREDENTIALLESS },
153+
{ ngx_string("unsafe-none"), NGX_HTTP_COEP_HEADER_UNSAFE_NONE },
154+
{ ngx_string("omit"), NGX_HTTP_SECURITY_HEADER_OMIT },
155+
{ ngx_null_string, 0 }
156+
};
157+
116158
static ngx_int_t ngx_http_security_headers_filter(ngx_http_request_t *r);
117159
static void *ngx_http_security_headers_create_loc_conf(ngx_conf_t *cf);
118160
static char *ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf,
@@ -180,6 +222,27 @@ static ngx_command_t ngx_http_security_headers_commands[] = {
180222
offsetof(ngx_http_security_headers_loc_conf_t, text_types_keys),
181223
&ngx_http_security_headers_default_text_types[0] },
182224

225+
{ ngx_string("security_headers_corp"),
226+
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
227+
ngx_conf_set_enum_slot,
228+
NGX_HTTP_LOC_CONF_OFFSET,
229+
offsetof(ngx_http_security_headers_loc_conf_t, corp),
230+
ngx_http_corp },
231+
232+
{ ngx_string("security_headers_coop"),
233+
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
234+
ngx_conf_set_enum_slot,
235+
NGX_HTTP_LOC_CONF_OFFSET,
236+
offsetof(ngx_http_security_headers_loc_conf_t, coop),
237+
ngx_http_coop },
238+
239+
{ ngx_string("security_headers_coep"),
240+
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
241+
ngx_conf_set_enum_slot,
242+
NGX_HTTP_LOC_CONF_OFFSET,
243+
offsetof(ngx_http_security_headers_loc_conf_t, coep),
244+
ngx_http_coep },
245+
183246
ngx_null_command
184247
};
185248

@@ -372,7 +435,77 @@ ngx_http_security_headers_filter(ngx_http_request_t *r)
372435
}
373436
}
374437

438+
/* Cross-Origin-Resource-Policy */
439+
if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED
440+
&& NGX_HTTP_SECURITY_HEADER_OMIT != slcf->corp)
441+
{
442+
switch (slcf->corp) {
443+
case NGX_HTTP_CORP_HEADER_SAME_SITE:
444+
ngx_str_set(&val, "same-site");
445+
break;
446+
case NGX_HTTP_CORP_HEADER_SAME_ORIGIN:
447+
ngx_str_set(&val, "same-origin");
448+
break;
449+
case NGX_HTTP_CORP_HEADER_CROSS_ORIGIN:
450+
ngx_str_set(&val, "cross-origin");
451+
break;
452+
default:
453+
val.len = 0;
454+
val.data = NULL;
455+
}
456+
if (val.data) {
457+
ngx_str_set(&key, "Cross-Origin-Resource-Policy");
458+
ngx_set_headers_out_by_search(r, &key, &val);
459+
}
460+
}
461+
462+
/* Cross-Origin-Opener-Policy */
463+
if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED
464+
&& NGX_HTTP_SECURITY_HEADER_OMIT != slcf->coop)
465+
{
466+
switch (slcf->coop) {
467+
case NGX_HTTP_COOP_HEADER_SAME_ORIGIN:
468+
ngx_str_set(&val, "same-origin");
469+
break;
470+
case NGX_HTTP_COOP_HEADER_SAME_ORIGIN_ALLOW_POPUPS:
471+
ngx_str_set(&val, "same-origin-allow-popups");
472+
break;
473+
case NGX_HTTP_COOP_HEADER_UNSAFE_NONE:
474+
ngx_str_set(&val, "unsafe-none");
475+
break;
476+
default:
477+
val.len = 0;
478+
val.data = NULL;
479+
}
480+
if (val.data) {
481+
ngx_str_set(&key, "Cross-Origin-Opener-Policy");
482+
ngx_set_headers_out_by_search(r, &key, &val);
483+
}
484+
}
375485

486+
/* Cross-Origin-Embedder-Policy */
487+
if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED
488+
&& NGX_HTTP_SECURITY_HEADER_OMIT != slcf->coep)
489+
{
490+
switch (slcf->coep) {
491+
case NGX_HTTP_COEP_HEADER_REQUIRE_CORP:
492+
ngx_str_set(&val, "require-corp");
493+
break;
494+
case NGX_HTTP_COEP_HEADER_CREDENTIALLESS:
495+
ngx_str_set(&val, "credentialless");
496+
break;
497+
case NGX_HTTP_COEP_HEADER_UNSAFE_NONE:
498+
ngx_str_set(&val, "unsafe-none");
499+
break;
500+
default:
501+
val.len = 0;
502+
val.data = NULL;
503+
}
504+
if (val.data) {
505+
ngx_str_set(&key, "Cross-Origin-Embedder-Policy");
506+
ngx_set_headers_out_by_search(r, &key, &val);
507+
}
508+
}
376509

377510
/* proceed to the next handler in chain */
378511
return ngx_http_next_header_filter(r);
@@ -392,6 +525,9 @@ ngx_http_security_headers_create_loc_conf(ngx_conf_t *cf)
392525
conf->xss = NGX_CONF_UNSET_UINT;
393526
conf->fo = NGX_CONF_UNSET_UINT;
394527
conf->rp = NGX_CONF_UNSET_UINT;
528+
conf->corp = NGX_CONF_UNSET_UINT;
529+
conf->coop = NGX_CONF_UNSET_UINT;
530+
conf->coep = NGX_CONF_UNSET_UINT;
395531
conf->enable = NGX_CONF_UNSET;
396532
conf->hide_server_tokens = NGX_CONF_UNSET_UINT;
397533
conf->hsts_preload = NGX_CONF_UNSET_UINT;
@@ -426,6 +562,14 @@ ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf, void *parent,
426562
ngx_conf_merge_uint_value(conf->rp, prev->rp,
427563
NGX_HTTP_RP_HEADER_STRICT_ORIG_WHEN_CROSS);
428564

565+
/* CORP defaults to same-site (opt-out), COOP/COEP default to omit (opt-in) */
566+
ngx_conf_merge_uint_value(conf->corp, prev->corp,
567+
NGX_HTTP_CORP_HEADER_SAME_SITE);
568+
ngx_conf_merge_uint_value(conf->coop, prev->coop,
569+
NGX_HTTP_SECURITY_HEADER_OMIT);
570+
ngx_conf_merge_uint_value(conf->coep, prev->coep,
571+
NGX_HTTP_SECURITY_HEADER_OMIT);
572+
429573
return NGX_CONF_OK;
430574
}
431575

0 commit comments

Comments
 (0)