header_guard 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +143 -2
- data/Gemfile.lock +1 -1
- data/README.md +135 -18
- data/lib/header_guard/middleware.rb +228 -26
- data/lib/header_guard/version.rb +1 -1
- data/lib/header_guard.rb +63 -30
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fde99ae71b2986c94287ef5b903ba52d00405019346f9c2f61a88c5c81dd24e
|
|
4
|
+
data.tar.gz: c7af2219fc4d05c19c2923184515921cc91f37e0cbbdb69ef65ae4d0b778c668
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74bb808854cc17a68c1a48d7c424e7e177dd30dcbbe4ae9ecfe5f5fa446475377c443a70a57529bc3a9e5d37dddcb2d39f3008a6da75bf5ddff6f5d6ebf3ee43
|
|
7
|
+
data.tar.gz: 226cbb1dd847db813eb69c236c59591440f31906683f8e644f1d3dd076e908aeda2c551abae859befd86b8b5909cd05946fa9530c9b0a33dd908efc18616f5b5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,146 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
See `PLAN.md` for the remaining
|
|
10
|
+
See `PLAN.md` for the remaining work (P4–P5).
|
|
11
|
+
|
|
12
|
+
## [0.3.0] - 2026-09-12
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Option validation.** Every option is checked at construction and raises `ArgumentError`
|
|
17
|
+
with a specific message. Previously an unrecognised Symbol key became a header name
|
|
18
|
+
verbatim, so `report_onlyy: true` emitted a junk `report_onlyy` header and silently
|
|
19
|
+
*enforced* a CSP the user meant only to report on. Now rejected: unknown Symbol options;
|
|
20
|
+
header names that are not valid HTTP tokens; `Content-Security-Policy` or its
|
|
21
|
+
`-Report-Only` variant given as a raw header (use the option); values that are not a
|
|
22
|
+
`String`; empty values; `report_only` / `html_only` values that are not `true` or `false`.
|
|
23
|
+
|
|
24
|
+
- **Control characters in header values are rejected.** A header value or CSP containing
|
|
25
|
+
CR, LF, NUL or any other control character raises at startup. A CR/LF in a value built
|
|
26
|
+
from configuration (a `report-uri` from an environment variable, say) would otherwise
|
|
27
|
+
let it inject further headers or split the response.
|
|
28
|
+
|
|
29
|
+
- **Removing a default header.** Set a header to `nil` (or `false`) and HeaderGuard stops
|
|
30
|
+
managing it: the default is not injected and any value the application sets itself
|
|
31
|
+
passes through untouched. Previously there was no way to opt out of a default; the README
|
|
32
|
+
suggested `""`, which emitted a malformed empty header.
|
|
33
|
+
|
|
34
|
+
- **Disabling the CSP.** `content_security_policy: false` stops HeaderGuard sending a CSP,
|
|
35
|
+
for applications that build one elsewhere (the Rails `content_security_policy` DSL, for
|
|
36
|
+
example) and want HeaderGuard for the other headers only. `content_security_policy: nil`
|
|
37
|
+
deliberately keeps the *default* policy, so an unset environment variable cannot silently
|
|
38
|
+
drop the CSP.
|
|
39
|
+
|
|
40
|
+
- **Per-path overrides.** `path_overrides: { matcher => options }` scopes a different
|
|
41
|
+
policy to particular routes — an identity provider's popup page that must keep
|
|
42
|
+
`window.opener`, an embeddable widget that other sites frame, a legacy page that still
|
|
43
|
+
needs inline scripts — without relaxing anything site-wide. A `Regexp` key is matched
|
|
44
|
+
against the request path; a `String` key must match exactly. First match wins. The value
|
|
45
|
+
is an options hash of the same shape as the top level (headers, `content_security_policy:`,
|
|
46
|
+
`report_only:`, `html_only:`), layered on top of the global configuration.
|
|
47
|
+
`Strict-Transport-Security` is refused inside an override: HSTS is host-scoped, not
|
|
48
|
+
per-document, so a weaker value on one path would apply to the whole site.
|
|
49
|
+
|
|
50
|
+
### Upgrading from 0.2.x
|
|
51
|
+
|
|
52
|
+
- Configurations that were accepted but wrong now fail at startup. If HeaderGuard raises
|
|
53
|
+
`ArgumentError` on boot, the message names the offending option. The common cases: a
|
|
54
|
+
misspelled Symbol option, and `"Strict-Transport-Security" => ""` from the old README
|
|
55
|
+
advice — replace with `nil`.
|
|
56
|
+
|
|
57
|
+
## [0.2.0] - 2026-09-12
|
|
58
|
+
|
|
59
|
+
Widens which responses receive security headers and hardens every default. Read
|
|
60
|
+
**Upgrading from 0.1.x** at the end of this entry before deploying.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **Standard headers are now applied to every response.** `Strict-Transport-Security`,
|
|
65
|
+
`X-Content-Type-Options`, `X-Frame-Options` and `Referrer-Policy` were previously
|
|
66
|
+
injected only on 2xx responses with an HTML content type, which excluded the responses
|
|
67
|
+
that need them most: JSON bodies got no `nosniff`, and the HTTP→HTTPS redirect — the
|
|
68
|
+
response where HSTS matters — got no HSTS. They now go on every response regardless
|
|
69
|
+
of status code or content type.
|
|
70
|
+
|
|
71
|
+
- **The Content Security Policy is now applied to HTML responses of every status.**
|
|
72
|
+
Previously only 2xx HTML responses received a CSP, leaving 4xx/5xx error pages
|
|
73
|
+
unprotected. Error pages routinely reflect user input and are a classic XSS surface.
|
|
74
|
+
CSP remains restricted to HTML content types, as it governs documents only.
|
|
75
|
+
|
|
76
|
+
- `application/xhtml+xml` is now treated as HTML for CSP purposes alongside `text/html`.
|
|
77
|
+
|
|
78
|
+
- **`preload` removed from the default `Strict-Transport-Security`.** Preload is the
|
|
79
|
+
opt-in signal for the browser HSTS preload list, which hard-codes the apex domain and
|
|
80
|
+
every subdomain as HTTPS-only inside the browser itself; removal takes months. That is
|
|
81
|
+
a commitment to make explicitly, not one to acquire by adding a middleware. The default
|
|
82
|
+
is now `max-age=31536000; includeSubDomains`. Opt back in by overriding the header.
|
|
83
|
+
|
|
84
|
+
- **Default CSP tightened to a strict same-origin baseline.** `style-src` was
|
|
85
|
+
`'self' 'unsafe-inline' https:` and `font-src` was `'self' https: data:`. Both are now
|
|
86
|
+
`'self'`. `https:` as a source allows content from any HTTPS origin and `'unsafe-inline'`
|
|
87
|
+
allows arbitrary inline styles — either lets an attacker who can inject markup load or
|
|
88
|
+
run content of their choosing, which is a weak default for something billed as a secure
|
|
89
|
+
baseline. Applications that need inline styles should add `'unsafe-inline'` deliberately
|
|
90
|
+
via `content_security_policy:`.
|
|
91
|
+
|
|
92
|
+
- Default CSP directives are now separated by `"; "` rather than `";"` for readability.
|
|
93
|
+
Semantically identical.
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- `html_only: true` option, which restores the 0.1.x behaviour of injecting nothing
|
|
98
|
+
unless the response is a 2xx with an HTML content type. This is a migration aid for
|
|
99
|
+
applications that depended on the narrower scope, not a recommended configuration.
|
|
100
|
+
|
|
101
|
+
- **Four new default headers:**
|
|
102
|
+
- `Cross-Origin-Opener-Policy: same-origin-allow-popups` — isolates the browsing
|
|
103
|
+
context from cross-origin openers, mitigating XS-Leaks and Spectre-class attacks,
|
|
104
|
+
while still allowing popups the site itself opens (OAuth/OIDC providers in popup
|
|
105
|
+
mode) to talk back via `window.opener`. Chosen over the stricter `same-origin`
|
|
106
|
+
because the gem targets SSO applications, where popup-based client SDKs are common;
|
|
107
|
+
the only protection given up is against windows the site's own code chose to open.
|
|
108
|
+
- `Cross-Origin-Resource-Policy: same-origin` — stops other origins embedding this
|
|
109
|
+
site's resources via no-cors requests.
|
|
110
|
+
- `X-Permitted-Cross-Domain-Policies: none` — forbids Flash/Acrobat cross-domain
|
|
111
|
+
policy files.
|
|
112
|
+
- `Permissions-Policy` — denies `accelerometer`, `camera`, `geolocation`, `gyroscope`,
|
|
113
|
+
`magnetometer`, `microphone`, `payment` and `usb` unless explicitly enabled.
|
|
114
|
+
|
|
115
|
+
### Removed
|
|
116
|
+
|
|
117
|
+
- `block-all-mixed-content` from the default CSP. The directive is deprecated and was
|
|
118
|
+
removed from CSP Level 3; `upgrade-insecure-requests`, which remains, covers it.
|
|
119
|
+
|
|
120
|
+
### Upgrading from 0.1.x
|
|
121
|
+
|
|
122
|
+
Read this section before deploying — 0.2.0 tightens several defaults and some
|
|
123
|
+
applications will need to opt back into behaviour they relied on.
|
|
124
|
+
|
|
125
|
+
- **Standard headers now land on every response.** If your application sets its own
|
|
126
|
+
value for one of them on a non-HTML response (for example a different
|
|
127
|
+
`X-Frame-Options` on an API endpoint), HeaderGuard now overwrites it there too, as it
|
|
128
|
+
always has on HTML. Pass the desired value as a custom header option, or use
|
|
129
|
+
`html_only: true` while you migrate.
|
|
130
|
+
- **Inline styles are now blocked by the default CSP.** If your pages use inline
|
|
131
|
+
`<style>` or `style=""` attributes (CSS-in-JS libraries commonly do), pass a
|
|
132
|
+
`content_security_policy:` that adds `'unsafe-inline'` to `style-src`, or better, move
|
|
133
|
+
to nonces.
|
|
134
|
+
- **Third-party fonts and stylesheets are now blocked by the default CSP.** Add the
|
|
135
|
+
specific origins you use (e.g. `https://fonts.googleapis.com`) to `style-src` and
|
|
136
|
+
`font-src` rather than reinstating `https:`.
|
|
137
|
+
- **Identity providers serving popup-based flows need to override
|
|
138
|
+
`Cross-Origin-Opener-Policy`.** The default `same-origin-allow-popups` keeps flows
|
|
139
|
+
where *your* site opens the popup working, but if your site *is* the popup (you are
|
|
140
|
+
the identity provider), the page the client opens must keep `window.opener`, which
|
|
141
|
+
requires `unsafe-none`. Redirect-based flows are unaffected either way.
|
|
142
|
+
- **Assets embedded by other sites will be blocked by `Cross-Origin-Resource-Policy`.**
|
|
143
|
+
If your app serves images, scripts or fonts meant to load on other origins, override
|
|
144
|
+
with `cross-origin`.
|
|
145
|
+
- **Features denied by `Permissions-Policy`** (camera, microphone, geolocation, payment,
|
|
146
|
+
and so on) must be re-enabled explicitly if your app uses them.
|
|
147
|
+
- **HSTS preload is no longer set.** If you had already submitted your domain to the
|
|
148
|
+
preload list, add `preload` back via a custom header — otherwise the list's periodic
|
|
149
|
+
checks will flag the domain for removal.
|
|
11
150
|
|
|
12
151
|
## [0.1.2] - 2026-09-08
|
|
13
152
|
|
|
@@ -64,7 +203,9 @@ See `PLAN.md` for the remaining remediation work (P1–P5), targeted at 0.2.0.
|
|
|
64
203
|
`X-Frame-Options`, `Referrer-Policy` and a configurable Content Security Policy,
|
|
65
204
|
with `report_only` support.
|
|
66
205
|
|
|
67
|
-
[Unreleased]: https://github.com/danielefrisanco/headerguard/compare/v0.
|
|
206
|
+
[Unreleased]: https://github.com/danielefrisanco/headerguard/compare/v0.3.0...HEAD
|
|
207
|
+
[0.3.0]: https://github.com/danielefrisanco/headerguard/compare/v0.2.0...v0.3.0
|
|
208
|
+
[0.2.0]: https://github.com/danielefrisanco/headerguard/compare/v0.1.2...v0.2.0
|
|
68
209
|
[0.1.2]: https://github.com/danielefrisanco/headerguard/compare/v0.1.1...v0.1.2
|
|
69
210
|
[0.1.1]: https://github.com/danielefrisanco/headerguard/compare/v0.1.0...v0.1.1
|
|
70
211
|
[0.1.0]: https://github.com/danielefrisanco/headerguard/releases/tag/v0.1.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -60,12 +60,14 @@ If you only want to relax one or two headers (like HSTS) but keep the others, yo
|
|
|
60
60
|
header_options = {}
|
|
61
61
|
|
|
62
62
|
if Rails.env.development? || Rails.env.test?
|
|
63
|
-
# 1.
|
|
64
|
-
|
|
63
|
+
# 1. Don't send Strict-Transport-Security for local HTTP development.
|
|
64
|
+
# nil means "HeaderGuard does not manage this header". (An empty string
|
|
65
|
+
# is rejected: it would send a malformed header.)
|
|
66
|
+
header_options["Strict-Transport-Security"] = nil
|
|
65
67
|
|
|
66
|
-
# 2. Relax CSP to allow development tools that rely on 'unsafe-inline' scripts/styles
|
|
67
|
-
#
|
|
68
|
-
dev_csp = "script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
|
|
68
|
+
# 2. Relax CSP to allow development tools that rely on 'unsafe-inline' scripts/styles.
|
|
69
|
+
# This replaces the whole default policy, so include every directive you still want.
|
|
70
|
+
dev_csp = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'"
|
|
69
71
|
header_options[:content_security_policy] = dev_csp
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -82,14 +84,18 @@ When integrating `HeaderGuard` into your project, you can pass an options hash t
|
|
|
82
84
|
|
|
83
85
|
#### 1\. Overriding Standard Headers
|
|
84
86
|
|
|
85
|
-
Any key/value pair passed to the middleware that matches a standard header will override the default value.
|
|
87
|
+
Any key/value pair passed to the middleware that matches a standard header will override the default value. Header names are matched case-insensitively.
|
|
86
88
|
|
|
87
|
-
| Header | Default Value | Purpose |
|
|
88
|
-
|
|
89
|
-
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains
|
|
90
|
-
| `X-Content-Type-Options` | `nosniff` | Prevents browser MIME-sniffing. |
|
|
91
|
-
| `X-Frame-Options` | `DENY` | Prevents clickjacking (set to SAMEORIGIN to allow framing on the same site). |
|
|
92
|
-
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Controls referrer information sent with requests. |
|
|
89
|
+
| Header | Default Value | Purpose |
|
|
90
|
+
| ----- | ----- | ----- |
|
|
91
|
+
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Enforces HTTPS usage. |
|
|
92
|
+
| `X-Content-Type-Options` | `nosniff` | Prevents browser MIME-sniffing. |
|
|
93
|
+
| `X-Frame-Options` | `DENY` | Prevents clickjacking (set to `SAMEORIGIN` to allow framing on the same site). |
|
|
94
|
+
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Controls referrer information sent with requests. |
|
|
95
|
+
| `Cross-Origin-Opener-Policy` | `same-origin-allow-popups` | Isolates the browsing context from cross-origin openers (XS-Leaks, Spectre) while allowing popups you open. |
|
|
96
|
+
| `Cross-Origin-Resource-Policy` | `same-origin` | Stops other origins embedding your resources via no-cors requests. |
|
|
97
|
+
| `X-Permitted-Cross-Domain-Policies` | `none` | Forbids Flash/Acrobat cross-domain policy files. |
|
|
98
|
+
| `Permissions-Policy` | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()` | Denies sensitive device features unless enabled. |
|
|
93
99
|
|
|
94
100
|
**Example: Overriding X-Frame-Options and Referrer-Policy:**
|
|
95
101
|
```ruby
|
|
@@ -98,14 +104,55 @@ config.middleware.use HeaderGuard::Middleware,
|
|
|
98
104
|
"X-Frame-Options" => "SAMEORIGIN",
|
|
99
105
|
"Referrer-Policy" => "no-referrer"
|
|
100
106
|
```
|
|
107
|
+
|
|
108
|
+
**Opting into HSTS preload.** The default deliberately omits `preload`. It is the signal for the [browser HSTS preload list](https://hstspreload.org/), which hard-codes your apex domain *and every subdomain* as HTTPS-only inside the browser, and removal takes months. Add it only once you've confirmed every subdomain serves HTTPS:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
config.middleware.use HeaderGuard::Middleware,
|
|
112
|
+
"Strict-Transport-Security" => "max-age=31536000; includeSubDomains; preload"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**`Cross-Origin-Opener-Policy` and popup-based auth.** The default `same-origin-allow-popups` means no cross-origin page can open your site and keep a handle on it, while popups *your* site opens — an OAuth/OIDC provider in popup mode — can still talk back via `window.opener`. Redirect-based flows are unaffected either way.
|
|
116
|
+
|
|
117
|
+
Two situations need a different value:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Your site *is* the popup (you are the identity provider): the page the client
|
|
121
|
+
# opens must keep window.opener, which requires disabling isolation on it.
|
|
122
|
+
config.middleware.use HeaderGuard::Middleware, "Cross-Origin-Opener-Policy" => "unsafe-none"
|
|
123
|
+
|
|
124
|
+
# You open no popups and want the strictest isolation available (also the
|
|
125
|
+
# value required, together with COEP, for cross-origin isolated features such
|
|
126
|
+
# as SharedArrayBuffer):
|
|
127
|
+
config.middleware.use HeaderGuard::Middleware, "Cross-Origin-Opener-Policy" => "same-origin"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Assets embedded by other sites and `Cross-Origin-Resource-Policy`.** `same-origin` prevents other origins from loading your images, scripts or fonts. If your app serves assets meant to be embedded elsewhere, set it to `cross-origin`.
|
|
101
131
|
#### 2\. Custom Content Security Policy (CSP)
|
|
102
132
|
|
|
103
|
-
|
|
133
|
+
The default CSP is a strict same-origin baseline:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
default-src 'self'; base-uri 'self'; font-src 'self'; form-action 'self';
|
|
137
|
+
frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self';
|
|
138
|
+
upgrade-insecure-requests
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Resources may load only from your own origin; plugins, `<base>` tags and object embeds are blocked; inline scripts and styles are not permitted. Most real applications will need to extend this. You can define a custom CSP string to replace the default:
|
|
104
142
|
|
|
105
143
|
```ruby
|
|
106
|
-
custom_csp = "default-src 'self'; script-src 'self'
|
|
144
|
+
custom_csp = "default-src 'self'; script-src 'self' https://trusted.cdn.com;"
|
|
107
145
|
use HeaderGuard::Middleware, content_security_policy: custom_csp
|
|
108
146
|
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When extending the policy, add the specific origins you need rather than broad sources. `https:` as a source allows content from *any* HTTPS origin, and `'unsafe-inline'` allows any inline style or script — both let an attacker who can inject markup load or run content of their choosing. If you must allow inline styles (many CSS-in-JS libraries need it), do so knowingly:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Allowing inline styles, explicitly.
|
|
153
|
+
use HeaderGuard::Middleware,
|
|
154
|
+
content_security_policy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'"
|
|
155
|
+
|
|
109
156
|
```
|
|
110
157
|
#### 3\. Report-Only Mode
|
|
111
158
|
|
|
@@ -116,18 +163,88 @@ To test a new CSP without enforcing it, set the report\_only option to true. Thi
|
|
|
116
163
|
config.middleware.use HeaderGuard::Middleware, report_only: true
|
|
117
164
|
|
|
118
165
|
```
|
|
166
|
+
#### 4\. Restricting to HTML Responses (legacy behaviour)
|
|
167
|
+
|
|
168
|
+
By default HeaderGuard applies its standard headers to **every** response and the CSP to every **HTML** response, whatever the status code (see *How It Works* below). Versions before 0.2.0 injected nothing unless the response was a 2xx with an HTML content type. If you depend on that narrower behaviour, set `html_only`:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# 0.1.x behaviour: inject only on 2xx text/html responses.
|
|
172
|
+
config.middleware.use HeaderGuard::Middleware, html_only: true
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
This is a migration aid, not a recommended configuration: it leaves JSON responses without `X-Content-Type-Options`, redirects without HSTS, and error pages without a CSP.
|
|
176
|
+
|
|
177
|
+
#### 5\. Removing a Default Header
|
|
178
|
+
|
|
179
|
+
Set a header to `nil` (or `false`) and HeaderGuard stops managing it: the default is not injected, and any value your application sets itself passes through untouched.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Don't send X-Frame-Options; the app sets its own where it needs one.
|
|
183
|
+
config.middleware.use HeaderGuard::Middleware, "X-Frame-Options" => nil
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To stop HeaderGuard sending a CSP at all — for example because your application builds one elsewhere, such as with the Rails `content_security_policy` DSL — pass `false`:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# HeaderGuard manages every header except the CSP.
|
|
191
|
+
config.middleware.use HeaderGuard::Middleware, content_security_policy: false
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Note that `content_security_policy: nil` keeps the **default** policy rather than disabling it, so an unset environment variable (`content_security_policy: ENV["CSP"]`) cannot silently drop your CSP. Only an explicit `false` turns it off.
|
|
196
|
+
|
|
197
|
+
#### 6\. Per-Path Overrides
|
|
198
|
+
|
|
199
|
+
Some routes legitimately need a different policy from the rest of the site: an OAuth callback page that must keep `window.opener`, a widget that other sites embed in a frame, a legacy admin page that still needs inline scripts. Rather than relaxing a header site-wide for the sake of one route, scope the change to that route:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
config.middleware.use HeaderGuard::Middleware,
|
|
203
|
+
path_overrides: {
|
|
204
|
+
# You are the identity provider: the popup page the client opens must keep window.opener.
|
|
205
|
+
%r{\A/oauth/authorize\z} => { "Cross-Origin-Opener-Policy" => "unsafe-none" },
|
|
206
|
+
|
|
207
|
+
# An embeddable widget: framed by other sites, its assets loaded cross-origin.
|
|
208
|
+
%r{\A/embed/} => {
|
|
209
|
+
"X-Frame-Options" => nil,
|
|
210
|
+
"Cross-Origin-Resource-Policy" => "cross-origin",
|
|
211
|
+
content_security_policy: "default-src 'self'; frame-ancestors *"
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
# Trial a stricter policy on one page before rolling it out.
|
|
215
|
+
"/checkout" => { content_security_policy: "default-src 'none'; script-src 'self'", report_only: true }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- A **`Regexp`** key is matched against the request path; a **`String`** key must match the path exactly (it is not a prefix — use a `Regexp` for prefixes). The first matching entry wins.
|
|
221
|
+
- The value is an options hash with the same shape as the top level: header overrides, `content_security_policy:`, `report_only:`, `html_only:`. It is layered **on top of** your global configuration, so a path inherits everything it doesn't mention.
|
|
222
|
+
- **`Strict-Transport-Security` cannot be overridden per path** and HeaderGuard will refuse to start if you try. HSTS is host-scoped, not per-document: a weaker value sent on one path would update the browser's policy for the entire site.
|
|
223
|
+
|
|
224
|
+
Anchor your patterns (`\A`, `\z`). `%r{/auth}` also matches `/authors`.
|
|
225
|
+
|
|
226
|
+
#### 7\. Validation
|
|
227
|
+
|
|
228
|
+
Every option is checked when the middleware is constructed, and HeaderGuard raises `ArgumentError` with a specific message rather than starting with a weakened policy:
|
|
229
|
+
|
|
230
|
+
- an unrecognised Symbol option (`reprot_only: true` used to emit a junk header and silently *enforce* the CSP it was meant to only report on);
|
|
231
|
+
- a header name that isn't a valid HTTP token, or that names the CSP (use `content_security_policy:` instead);
|
|
232
|
+
- a header value or CSP that isn't a `String`, is empty, or contains a control character — a CR/LF would let a value injected from configuration split the response;
|
|
233
|
+
- `report_only` / `html_only` values that aren't `true` or `false`;
|
|
234
|
+
- `Strict-Transport-Security` inside a path override.
|
|
119
235
|
|
|
120
236
|
How It Works
|
|
121
237
|
------------
|
|
122
238
|
|
|
123
|
-
HeaderGuard hooks into the Rack request lifecycle and
|
|
239
|
+
HeaderGuard hooks into the Rack request lifecycle and, on every response passing through it:
|
|
124
240
|
|
|
125
|
-
1. **Header Merging:**
|
|
241
|
+
1. **Header Merging:** At startup it validates your options, then merges any custom headers over the defaults, ensuring user configuration takes precedence. Per-path overrides are layered over that once more. On each request it picks the policy for the request path.
|
|
126
242
|
|
|
127
|
-
2. **Injection:** It injects the
|
|
243
|
+
2. **Standard Header Injection:** It injects every header in the table above (HSTS, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, the cross-origin isolation headers and `Permissions-Policy`) on **every** response, regardless of status code or content type. HSTS matters most on the HTTP→HTTPS redirect, and `nosniff` exists precisely to protect non-HTML bodies such as JSON.
|
|
128
244
|
|
|
129
|
-
3. **CSP Injection:** It injects the configured Content Security Policy
|
|
245
|
+
3. **CSP Injection:** It injects the configured Content Security Policy — using either the standard enforcement header or the Report-Only header — on every response whose `Content-Type` is `text/html` or `application/xhtml+xml`, **including error pages**. Error pages routinely reflect user input and are a classic XSS surface, so they need a policy at least as much as a 200 does. Non-HTML responses do not receive a CSP, as it governs documents only.
|
|
130
246
|
|
|
247
|
+
Header names are handled case-insensitively and always written in lowercase, as the Rack 3 SPEC requires, so both Rack 2 and Rack 3 style applications are supported.
|
|
131
248
|
|
|
132
249
|
Development
|
|
133
250
|
-----------
|
|
@@ -7,65 +7,267 @@ module HeaderGuard
|
|
|
7
7
|
# requires response header keys to be lowercase, while Rack 2 applications
|
|
8
8
|
# conventionally use capitalized keys, so this middleware reads incoming
|
|
9
9
|
# headers case-insensitively and always writes lowercase keys.
|
|
10
|
+
#
|
|
11
|
+
# Which responses receive which headers:
|
|
12
|
+
#
|
|
13
|
+
# * The standard headers (HSTS, X-Content-Type-Options, X-Frame-Options,
|
|
14
|
+
# Referrer-Policy, ...) are applied to every response, regardless of status
|
|
15
|
+
# or content type. HSTS matters most on the HTTP->HTTPS redirect, and
|
|
16
|
+
# nosniff exists precisely to protect non-HTML bodies such as JSON.
|
|
17
|
+
# * The Content Security Policy governs a document, so it is applied only to
|
|
18
|
+
# HTML responses -- but on every status. Error pages reflect user input and
|
|
19
|
+
# are a classic XSS surface, so they need a policy at least as much as a
|
|
20
|
+
# 200 does.
|
|
21
|
+
#
|
|
22
|
+
# Options are validated at construction and raise ArgumentError on anything
|
|
23
|
+
# unrecognised or malformed, so a typo cannot silently weaken the policy.
|
|
10
24
|
class Middleware
|
|
25
|
+
# Content types treated as HTML documents for the purpose of applying CSP.
|
|
26
|
+
HTML_CONTENT_TYPES = ["text/html", "application/xhtml+xml"].freeze
|
|
27
|
+
|
|
28
|
+
# Symbol keys with special meaning. Every other key must be a String naming
|
|
29
|
+
# a header; any other Symbol is a typo and is rejected.
|
|
30
|
+
OPTION_KEYS = %i[content_security_policy report_only html_only path_overrides].freeze
|
|
31
|
+
|
|
32
|
+
# Headers set through their own option rather than as a raw header, so the
|
|
33
|
+
# two mechanisms cannot silently fight over the same response header.
|
|
34
|
+
RESERVED_HEADERS = ["content-security-policy", "content-security-policy-report-only"].freeze
|
|
35
|
+
|
|
36
|
+
# Headers whose effect is host-wide rather than per-document. Sending a
|
|
37
|
+
# weaker value on one path would weaken it for the whole site, so they may
|
|
38
|
+
# not appear in path_overrides.
|
|
39
|
+
HOST_SCOPED_HEADERS = ["strict-transport-security"].freeze
|
|
40
|
+
|
|
41
|
+
# The RFC 7230 token alphabet: the only characters legal in a header name.
|
|
42
|
+
HEADER_NAME = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/.freeze
|
|
43
|
+
|
|
44
|
+
# Any control character in a header value is rejected. CR and LF in
|
|
45
|
+
# particular would let a value inject further headers or a whole second
|
|
46
|
+
# response (response splitting).
|
|
47
|
+
CONTROL_CHARS = /[\x00-\x1F\x7F]/.freeze
|
|
48
|
+
|
|
49
|
+
CSP_HEADER = "content-security-policy"
|
|
50
|
+
CSP_REPORT_ONLY_HEADER = "content-security-policy-report-only"
|
|
51
|
+
|
|
52
|
+
# Fully resolved configuration for one scope: the whole site, or one path.
|
|
53
|
+
# `csp_value` is nil when CSP is disabled for the scope.
|
|
54
|
+
Policy = Struct.new(:headers, :csp_key, :csp_value, :html_only, keyword_init: true)
|
|
55
|
+
|
|
11
56
|
# Initializes the middleware. It merges user-defined options over defaults.
|
|
12
57
|
#
|
|
13
58
|
# @param app [Object] The next application in the Rack stack.
|
|
14
59
|
# @param options [Hash] Configuration options for headers and CSP.
|
|
60
|
+
# @raise [ArgumentError] on any unknown option, malformed header name or
|
|
61
|
+
# value, or a path override that would weaken a host-scoped header.
|
|
15
62
|
def initialize(app, options = {})
|
|
16
63
|
@app = app
|
|
17
64
|
|
|
18
|
-
|
|
19
|
-
config = options.dup
|
|
20
|
-
|
|
21
|
-
# Extract special configuration settings
|
|
22
|
-
custom_csp = config.delete(:content_security_policy)
|
|
23
|
-
@report_only = config.delete(:report_only) || false
|
|
65
|
+
raise ArgumentError, "HeaderGuard options must be a Hash, got #{options.class}" unless options.is_a?(Hash)
|
|
24
66
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
normalized[normalize_key(key)] = value
|
|
32
|
-
end.freeze
|
|
67
|
+
defaults = Policy.new(
|
|
68
|
+
headers: normalize_keys(DEFAULT_HEADERS),
|
|
69
|
+
csp_key: CSP_HEADER,
|
|
70
|
+
csp_value: DEFAULT_CSP,
|
|
71
|
+
html_only: false
|
|
72
|
+
)
|
|
33
73
|
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
@csp_header_key = @report_only ? "content-security-policy-report-only" : "content-security-policy"
|
|
74
|
+
@policy = build_policy(options, defaults, path: nil)
|
|
75
|
+
@path_policies = build_path_policies(options[:path_overrides])
|
|
37
76
|
end
|
|
38
77
|
|
|
39
78
|
# The Rack application call method.
|
|
40
79
|
def call(env)
|
|
41
80
|
status, headers, body = @app.call(env)
|
|
42
81
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
policy = policy_for(env["PATH_INFO"])
|
|
83
|
+
html = html?(headers)
|
|
84
|
+
|
|
85
|
+
if apply_standard_headers?(policy, status, html)
|
|
86
|
+
policy.headers.each do |key, value|
|
|
48
87
|
# We use assignment here, not `||=`, to ensure the middleware
|
|
49
88
|
# overwrites any headers set by the application before it,
|
|
50
89
|
# adhering to the strong security posture.
|
|
51
90
|
assign(headers, key, value)
|
|
52
91
|
end
|
|
53
|
-
|
|
54
|
-
# Inject the configured CSP header
|
|
55
|
-
assign(headers, @csp_header_key, @csp_value)
|
|
56
92
|
end
|
|
57
93
|
|
|
94
|
+
assign(headers, policy.csp_key, policy.csp_value) if policy.csp_value && apply_csp?(policy, status, html)
|
|
95
|
+
|
|
58
96
|
[status, headers, body]
|
|
59
97
|
end
|
|
60
98
|
|
|
61
99
|
private
|
|
62
100
|
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
# Configuration
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
# Layers one options Hash over a base Policy. Used once for the global
|
|
106
|
+
# options over the defaults, and once per path override over the global
|
|
107
|
+
# policy, so an override inherits everything it does not mention.
|
|
108
|
+
def build_policy(options, base, path:)
|
|
109
|
+
headers = base.headers.dup
|
|
110
|
+
csp_value = base.csp_value
|
|
111
|
+
report_only = base.csp_key == CSP_REPORT_ONLY_HEADER
|
|
112
|
+
html_only = base.html_only
|
|
113
|
+
|
|
114
|
+
options.each do |key, value|
|
|
115
|
+
case key
|
|
116
|
+
when :content_security_policy
|
|
117
|
+
csp_value = resolve_csp(value, csp_value)
|
|
118
|
+
when :report_only
|
|
119
|
+
report_only = resolve_flag(key, value, report_only)
|
|
120
|
+
when :html_only
|
|
121
|
+
html_only = resolve_flag(key, value, html_only)
|
|
122
|
+
when :path_overrides
|
|
123
|
+
raise ArgumentError, "path_overrides cannot be nested inside the override for #{path.inspect}" if path
|
|
124
|
+
when String
|
|
125
|
+
name = validate_header_name(key, path)
|
|
126
|
+
if value.nil? || value == false
|
|
127
|
+
headers.delete(name)
|
|
128
|
+
else
|
|
129
|
+
headers[name] = validate_header_value(key, value)
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
raise ArgumentError,
|
|
133
|
+
"unknown HeaderGuard option #{key.inspect}. Recognised options are " \
|
|
134
|
+
"#{OPTION_KEYS.map(&:inspect).join(', ')}; custom headers must be given as String keys."
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Policy.new(
|
|
139
|
+
headers: headers.freeze,
|
|
140
|
+
csp_key: report_only ? CSP_REPORT_ONLY_HEADER : CSP_HEADER,
|
|
141
|
+
csp_value: csp_value,
|
|
142
|
+
html_only: html_only
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_path_policies(overrides)
|
|
147
|
+
return [].freeze if overrides.nil?
|
|
148
|
+
|
|
149
|
+
unless overrides.is_a?(Hash)
|
|
150
|
+
raise ArgumentError, "path_overrides must be a Hash of path matcher => options, got #{overrides.class}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
overrides.map do |matcher, options|
|
|
154
|
+
unless matcher.is_a?(String) || matcher.is_a?(Regexp)
|
|
155
|
+
raise ArgumentError, "path_overrides keys must be a String (exact path) or a Regexp, got #{matcher.inspect}"
|
|
156
|
+
end
|
|
157
|
+
unless options.is_a?(Hash)
|
|
158
|
+
raise ArgumentError, "path_overrides[#{matcher.inspect}] must be an options Hash, got #{options.class}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[matcher, build_policy(options, @policy, path: matcher)]
|
|
162
|
+
end.freeze
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# nil keeps the inherited value (so an unset ENV var cannot silently
|
|
166
|
+
# disable CSP); false disables CSP for the scope; a String replaces it.
|
|
167
|
+
def resolve_csp(value, current)
|
|
168
|
+
case value
|
|
169
|
+
when nil then current
|
|
170
|
+
when false then nil
|
|
171
|
+
when String then validate_header_value(:content_security_policy, value)
|
|
172
|
+
else
|
|
173
|
+
raise ArgumentError, "content_security_policy must be a String, false, or nil, got #{value.inspect}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def resolve_flag(key, value, current)
|
|
178
|
+
return current if value.nil?
|
|
179
|
+
return value if value == true || value == false
|
|
180
|
+
|
|
181
|
+
raise ArgumentError, "#{key} must be true or false, got #{value.inspect}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def validate_header_name(key, path)
|
|
185
|
+
raise ArgumentError, "#{key.inspect} is not a valid HTTP header name" unless key.match?(HEADER_NAME)
|
|
186
|
+
|
|
187
|
+
name = key.downcase
|
|
188
|
+
|
|
189
|
+
if RESERVED_HEADERS.include?(name)
|
|
190
|
+
raise ArgumentError,
|
|
191
|
+
"#{key} cannot be set as a raw header; use the content_security_policy: and report_only: options"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if path && HOST_SCOPED_HEADERS.include?(name)
|
|
195
|
+
raise ArgumentError,
|
|
196
|
+
"#{key} cannot be overridden for #{path.inspect}: it is host-scoped, not per-document, " \
|
|
197
|
+
"so a value sent on one path would apply to the whole site"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
name
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def validate_header_value(key, value)
|
|
204
|
+
unless value.is_a?(String)
|
|
205
|
+
raise ArgumentError, "value for #{key.inspect} must be a String, got #{value.inspect}"
|
|
206
|
+
end
|
|
207
|
+
if value.empty?
|
|
208
|
+
raise ArgumentError, "value for #{key.inspect} is empty; pass nil to remove the header instead"
|
|
209
|
+
end
|
|
210
|
+
if value.match?(CONTROL_CHARS)
|
|
211
|
+
raise ArgumentError,
|
|
212
|
+
"value for #{key.inspect} contains a control character (CR, LF, ...), which would allow " \
|
|
213
|
+
"header injection or response splitting"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
value
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def normalize_keys(headers)
|
|
220
|
+
headers.each_with_object({}) do |(key, value), normalized|
|
|
221
|
+
normalized[normalize_key(key)] = value
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
63
225
|
def normalize_key(key)
|
|
64
226
|
key.to_s.downcase
|
|
65
227
|
end
|
|
66
228
|
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
# Per-request
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
# First matching override wins; none matching means the global policy.
|
|
234
|
+
def policy_for(path)
|
|
235
|
+
path = path.to_s
|
|
236
|
+
|
|
237
|
+
@path_policies.each do |matcher, policy|
|
|
238
|
+
matched = matcher.is_a?(Regexp) ? matcher.match?(path) : matcher == path
|
|
239
|
+
return policy if matched
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
@policy
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Standard headers go on every response. In html_only mode they are
|
|
246
|
+
# restricted to 2xx HTML, as in 0.1.x.
|
|
247
|
+
def apply_standard_headers?(policy, status, html)
|
|
248
|
+
return true unless policy.html_only
|
|
249
|
+
|
|
250
|
+
success?(status) && html
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# CSP goes on every HTML response regardless of status. In html_only mode
|
|
254
|
+
# it is restricted to 2xx HTML, as in 0.1.x.
|
|
255
|
+
def apply_csp?(policy, status, html)
|
|
256
|
+
return false unless html
|
|
257
|
+
return true unless policy.html_only
|
|
258
|
+
|
|
259
|
+
success?(status)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def success?(status)
|
|
263
|
+
(200..299).cover?(status)
|
|
264
|
+
end
|
|
265
|
+
|
|
67
266
|
def html?(headers)
|
|
68
|
-
fetch_header(headers, "content-type")
|
|
267
|
+
content_type = fetch_header(headers, "content-type")
|
|
268
|
+
return false unless content_type
|
|
269
|
+
|
|
270
|
+
HTML_CONTENT_TYPES.any? { |type| content_type.include?(type) }
|
|
69
271
|
end
|
|
70
272
|
|
|
71
273
|
# Rack 3 responses key headers in lowercase; Rack 2 applications typically
|
data/lib/header_guard/version.rb
CHANGED
data/lib/header_guard.rb
CHANGED
|
@@ -4,38 +4,71 @@ require_relative "header_guard/version"
|
|
|
4
4
|
require_relative "header_guard/middleware"
|
|
5
5
|
|
|
6
6
|
module HeaderGuard
|
|
7
|
-
# Standard security headers applied by default.
|
|
7
|
+
# Standard security headers applied to every response by default.
|
|
8
8
|
DEFAULT_HEADERS = {
|
|
9
|
-
# Strictly enforce HTTPS,
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
|
|
9
|
+
# Strictly enforce HTTPS for one year, including subdomains, preventing
|
|
10
|
+
# protocol downgrade attacks.
|
|
11
|
+
#
|
|
12
|
+
# `preload` is deliberately omitted. It is the opt-in signal for the browser
|
|
13
|
+
# HSTS preload list, which hard-codes the apex domain and every subdomain as
|
|
14
|
+
# HTTPS-only inside the browser itself, and removal from the list takes
|
|
15
|
+
# months. That is a commitment to make explicitly, not one to acquire by
|
|
16
|
+
# adding a middleware. Opt in with:
|
|
17
|
+
# "Strict-Transport-Security" => "max-age=31536000; includeSubDomains; preload"
|
|
18
|
+
"Strict-Transport-Security" => "max-age=31536000; includeSubDomains",
|
|
19
|
+
# Prevent the browser from guessing the content type, which can lead to XSS
|
|
20
|
+
# if it misinterprets a file as a script.
|
|
21
|
+
"X-Content-Type-Options" => "nosniff",
|
|
22
|
+
# Forbid rendering this site inside a frame, preventing clickjacking. The
|
|
23
|
+
# legacy counterpart of the CSP `frame-ancestors` directive below.
|
|
24
|
+
"X-Frame-Options" => "DENY",
|
|
25
|
+
# Send the full referrer only on same-origin requests, and just the origin
|
|
26
|
+
# cross-origin; a good balance of security and functionality.
|
|
27
|
+
"Referrer-Policy" => "strict-origin-when-cross-origin",
|
|
28
|
+
# Put this site in its own browsing context group, so a cross-origin window
|
|
29
|
+
# that opens it cannot hold a reference to it. Mitigates XS-Leaks and
|
|
30
|
+
# Spectre-class attacks.
|
|
31
|
+
#
|
|
32
|
+
# "same-origin-allow-popups" rather than the stricter "same-origin": it
|
|
33
|
+
# keeps the protection that matters (nobody can open *this* site and keep a
|
|
34
|
+
# handle on it) while still letting popups this site opens -- an OAuth/OIDC
|
|
35
|
+
# provider in popup mode, say -- talk back via `window.opener`. The only
|
|
36
|
+
# thing given up is isolation from windows this site's own code chose to
|
|
37
|
+
# open, and `noopener` covers that where it matters.
|
|
38
|
+
#
|
|
39
|
+
# If this site *is* the popup (you are the identity provider), the page the
|
|
40
|
+
# client opens needs "unsafe-none". Redirect-based flows are unaffected.
|
|
41
|
+
"Cross-Origin-Opener-Policy" => "same-origin-allow-popups",
|
|
42
|
+
# Prevent other origins from embedding this site's resources (images,
|
|
43
|
+
# scripts, fonts) via no-cors requests. Override with "cross-origin" on
|
|
44
|
+
# assets that are meant to be embedded elsewhere.
|
|
45
|
+
"Cross-Origin-Resource-Policy" => "same-origin",
|
|
46
|
+
# Forbid Adobe Flash / Acrobat cross-domain policy files.
|
|
47
|
+
"X-Permitted-Cross-Domain-Policies" => "none",
|
|
48
|
+
# Deny access to sensitive device features unless explicitly enabled.
|
|
49
|
+
"Permissions-Policy" => "accelerometer=(), camera=(), geolocation=(), gyroscope=(), " \
|
|
50
|
+
"magnetometer=(), microphone=(), payment=(), usb=()"
|
|
21
51
|
}.freeze
|
|
22
52
|
|
|
23
|
-
# Default Content Security Policy
|
|
24
|
-
# This serves as a strong baseline, allowing resources only from the same origin ('self'),
|
|
25
|
-
# and explicitly blocking all plugins, base tags, and object embeds.
|
|
53
|
+
# Default Content Security Policy: a strict same-origin baseline.
|
|
26
54
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
55
|
+
# Resources may load only from this origin; plugins, <base> tags and object
|
|
56
|
+
# embeds are blocked outright; inline scripts and styles are not permitted.
|
|
57
|
+
#
|
|
58
|
+
# A real application will need to extend this -- to allow a CDN, an analytics
|
|
59
|
+
# script, inline styles -- but should do so by adding the specific origins or
|
|
60
|
+
# nonces it needs, not by reintroducing broad sources such as `https:` or
|
|
61
|
+
# `'unsafe-inline'`. Both let an attacker who can inject markup load or run
|
|
62
|
+
# content from an origin of their choosing.
|
|
63
|
+
DEFAULT_CSP = [
|
|
64
|
+
"default-src 'self'",
|
|
65
|
+
"base-uri 'self'",
|
|
66
|
+
"font-src 'self'",
|
|
67
|
+
"form-action 'self'",
|
|
68
|
+
"frame-ancestors 'none'",
|
|
69
|
+
"object-src 'none'",
|
|
70
|
+
"script-src 'self'",
|
|
71
|
+
"style-src 'self'",
|
|
72
|
+
"upgrade-insecure-requests"
|
|
73
|
+
].join("; ").freeze
|
|
41
74
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: header_guard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gemini AI
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-09-
|
|
12
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rack
|