header_guard 0.1.2 → 0.2.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 +97 -2
- data/Gemfile.lock +1 -1
- data/README.md +69 -13
- data/lib/header_guard/middleware.rb +48 -8
- 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: ea2dc2323334a13252e61e46ada4dbc84657d0a0f998ee5b1d4481d7c10c0a1b
|
|
4
|
+
data.tar.gz: 840fa96c9bb743cd59c4aea1a92adf80c116ccb6615af5c9f4d1a34ea3c16b6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37cd5fd01fc7d57b0ad5ee1282115f5253552dcb93b9026bf78b8890b75d83d9781a3c85c821e37556432b94a2a9a36dda00eff1061582a51f12855e6b31c908
|
|
7
|
+
data.tar.gz: 18e56009b28987ccfcdbb78646d07fdf13f0ab9389c02e1d985aef41ec615f020c4012e4d01d3a69de8c48637e6b0de8e7b5ccdae81df15b9573f98b448864b0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,101 @@ 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 remediation work (
|
|
10
|
+
See `PLAN.md` for the remaining remediation work (P3–P5), targeted at 0.3.0.
|
|
11
|
+
|
|
12
|
+
## [0.2.0] - 2026-09-12
|
|
13
|
+
|
|
14
|
+
Widens which responses receive security headers and hardens every default. Read
|
|
15
|
+
**Upgrading from 0.1.x** at the end of this entry before deploying.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Standard headers are now applied to every response.** `Strict-Transport-Security`,
|
|
20
|
+
`X-Content-Type-Options`, `X-Frame-Options` and `Referrer-Policy` were previously
|
|
21
|
+
injected only on 2xx responses with an HTML content type, which excluded the responses
|
|
22
|
+
that need them most: JSON bodies got no `nosniff`, and the HTTP→HTTPS redirect — the
|
|
23
|
+
response where HSTS matters — got no HSTS. They now go on every response regardless
|
|
24
|
+
of status code or content type.
|
|
25
|
+
|
|
26
|
+
- **The Content Security Policy is now applied to HTML responses of every status.**
|
|
27
|
+
Previously only 2xx HTML responses received a CSP, leaving 4xx/5xx error pages
|
|
28
|
+
unprotected. Error pages routinely reflect user input and are a classic XSS surface.
|
|
29
|
+
CSP remains restricted to HTML content types, as it governs documents only.
|
|
30
|
+
|
|
31
|
+
- `application/xhtml+xml` is now treated as HTML for CSP purposes alongside `text/html`.
|
|
32
|
+
|
|
33
|
+
- **`preload` removed from the default `Strict-Transport-Security`.** Preload is the
|
|
34
|
+
opt-in signal for the browser HSTS preload list, which hard-codes the apex domain and
|
|
35
|
+
every subdomain as HTTPS-only inside the browser itself; removal takes months. That is
|
|
36
|
+
a commitment to make explicitly, not one to acquire by adding a middleware. The default
|
|
37
|
+
is now `max-age=31536000; includeSubDomains`. Opt back in by overriding the header.
|
|
38
|
+
|
|
39
|
+
- **Default CSP tightened to a strict same-origin baseline.** `style-src` was
|
|
40
|
+
`'self' 'unsafe-inline' https:` and `font-src` was `'self' https: data:`. Both are now
|
|
41
|
+
`'self'`. `https:` as a source allows content from any HTTPS origin and `'unsafe-inline'`
|
|
42
|
+
allows arbitrary inline styles — either lets an attacker who can inject markup load or
|
|
43
|
+
run content of their choosing, which is a weak default for something billed as a secure
|
|
44
|
+
baseline. Applications that need inline styles should add `'unsafe-inline'` deliberately
|
|
45
|
+
via `content_security_policy:`.
|
|
46
|
+
|
|
47
|
+
- Default CSP directives are now separated by `"; "` rather than `";"` for readability.
|
|
48
|
+
Semantically identical.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- `html_only: true` option, which restores the 0.1.x behaviour of injecting nothing
|
|
53
|
+
unless the response is a 2xx with an HTML content type. This is a migration aid for
|
|
54
|
+
applications that depended on the narrower scope, not a recommended configuration.
|
|
55
|
+
|
|
56
|
+
- **Four new default headers:**
|
|
57
|
+
- `Cross-Origin-Opener-Policy: same-origin-allow-popups` — isolates the browsing
|
|
58
|
+
context from cross-origin openers, mitigating XS-Leaks and Spectre-class attacks,
|
|
59
|
+
while still allowing popups the site itself opens (OAuth/OIDC providers in popup
|
|
60
|
+
mode) to talk back via `window.opener`. Chosen over the stricter `same-origin`
|
|
61
|
+
because the gem targets SSO applications, where popup-based client SDKs are common;
|
|
62
|
+
the only protection given up is against windows the site's own code chose to open.
|
|
63
|
+
- `Cross-Origin-Resource-Policy: same-origin` — stops other origins embedding this
|
|
64
|
+
site's resources via no-cors requests.
|
|
65
|
+
- `X-Permitted-Cross-Domain-Policies: none` — forbids Flash/Acrobat cross-domain
|
|
66
|
+
policy files.
|
|
67
|
+
- `Permissions-Policy` — denies `accelerometer`, `camera`, `geolocation`, `gyroscope`,
|
|
68
|
+
`magnetometer`, `microphone`, `payment` and `usb` unless explicitly enabled.
|
|
69
|
+
|
|
70
|
+
### Removed
|
|
71
|
+
|
|
72
|
+
- `block-all-mixed-content` from the default CSP. The directive is deprecated and was
|
|
73
|
+
removed from CSP Level 3; `upgrade-insecure-requests`, which remains, covers it.
|
|
74
|
+
|
|
75
|
+
### Upgrading from 0.1.x
|
|
76
|
+
|
|
77
|
+
Read this section before deploying — 0.2.0 tightens several defaults and some
|
|
78
|
+
applications will need to opt back into behaviour they relied on.
|
|
79
|
+
|
|
80
|
+
- **Standard headers now land on every response.** If your application sets its own
|
|
81
|
+
value for one of them on a non-HTML response (for example a different
|
|
82
|
+
`X-Frame-Options` on an API endpoint), HeaderGuard now overwrites it there too, as it
|
|
83
|
+
always has on HTML. Pass the desired value as a custom header option, or use
|
|
84
|
+
`html_only: true` while you migrate.
|
|
85
|
+
- **Inline styles are now blocked by the default CSP.** If your pages use inline
|
|
86
|
+
`<style>` or `style=""` attributes (CSS-in-JS libraries commonly do), pass a
|
|
87
|
+
`content_security_policy:` that adds `'unsafe-inline'` to `style-src`, or better, move
|
|
88
|
+
to nonces.
|
|
89
|
+
- **Third-party fonts and stylesheets are now blocked by the default CSP.** Add the
|
|
90
|
+
specific origins you use (e.g. `https://fonts.googleapis.com`) to `style-src` and
|
|
91
|
+
`font-src` rather than reinstating `https:`.
|
|
92
|
+
- **Identity providers serving popup-based flows need to override
|
|
93
|
+
`Cross-Origin-Opener-Policy`.** The default `same-origin-allow-popups` keeps flows
|
|
94
|
+
where *your* site opens the popup working, but if your site *is* the popup (you are
|
|
95
|
+
the identity provider), the page the client opens must keep `window.opener`, which
|
|
96
|
+
requires `unsafe-none`. Redirect-based flows are unaffected either way.
|
|
97
|
+
- **Assets embedded by other sites will be blocked by `Cross-Origin-Resource-Policy`.**
|
|
98
|
+
If your app serves images, scripts or fonts meant to load on other origins, override
|
|
99
|
+
with `cross-origin`.
|
|
100
|
+
- **Features denied by `Permissions-Policy`** (camera, microphone, geolocation, payment,
|
|
101
|
+
and so on) must be re-enabled explicitly if your app uses them.
|
|
102
|
+
- **HSTS preload is no longer set.** If you had already submitted your domain to the
|
|
103
|
+
preload list, add `preload` back via a custom header — otherwise the list's periodic
|
|
104
|
+
checks will flag the domain for removal.
|
|
11
105
|
|
|
12
106
|
## [0.1.2] - 2026-09-08
|
|
13
107
|
|
|
@@ -64,7 +158,8 @@ See `PLAN.md` for the remaining remediation work (P1–P5), targeted at 0.2.0.
|
|
|
64
158
|
`X-Frame-Options`, `Referrer-Policy` and a configurable Content Security Policy,
|
|
65
159
|
with `report_only` support.
|
|
66
160
|
|
|
67
|
-
[Unreleased]: https://github.com/danielefrisanco/headerguard/compare/v0.
|
|
161
|
+
[Unreleased]: https://github.com/danielefrisanco/headerguard/compare/v0.2.0...HEAD
|
|
162
|
+
[0.2.0]: https://github.com/danielefrisanco/headerguard/compare/v0.1.2...v0.2.0
|
|
68
163
|
[0.1.2]: https://github.com/danielefrisanco/headerguard/compare/v0.1.1...v0.1.2
|
|
69
164
|
[0.1.1]: https://github.com/danielefrisanco/headerguard/compare/v0.1.0...v0.1.1
|
|
70
165
|
[0.1.0]: https://github.com/danielefrisanco/headerguard/releases/tag/v0.1.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -82,14 +82,18 @@ When integrating `HeaderGuard` into your project, you can pass an options hash t
|
|
|
82
82
|
|
|
83
83
|
#### 1\. Overriding Standard Headers
|
|
84
84
|
|
|
85
|
-
Any key/value pair passed to the middleware that matches a standard header will override the default value.
|
|
86
|
-
|
|
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. |
|
|
85
|
+
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
|
+
|
|
87
|
+
| Header | Default Value | Purpose |
|
|
88
|
+
| ----- | ----- | ----- |
|
|
89
|
+
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Enforces HTTPS usage. |
|
|
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. |
|
|
93
|
+
| `Cross-Origin-Opener-Policy` | `same-origin-allow-popups` | Isolates the browsing context from cross-origin openers (XS-Leaks, Spectre) while allowing popups you open. |
|
|
94
|
+
| `Cross-Origin-Resource-Policy` | `same-origin` | Stops other origins embedding your resources via no-cors requests. |
|
|
95
|
+
| `X-Permitted-Cross-Domain-Policies` | `none` | Forbids Flash/Acrobat cross-domain policy files. |
|
|
96
|
+
| `Permissions-Policy` | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()` | Denies sensitive device features unless enabled. |
|
|
93
97
|
|
|
94
98
|
**Example: Overriding X-Frame-Options and Referrer-Policy:**
|
|
95
99
|
```ruby
|
|
@@ -98,14 +102,55 @@ config.middleware.use HeaderGuard::Middleware,
|
|
|
98
102
|
"X-Frame-Options" => "SAMEORIGIN",
|
|
99
103
|
"Referrer-Policy" => "no-referrer"
|
|
100
104
|
```
|
|
105
|
+
|
|
106
|
+
**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:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
config.middleware.use HeaderGuard::Middleware,
|
|
110
|
+
"Strict-Transport-Security" => "max-age=31536000; includeSubDomains; preload"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**`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.
|
|
114
|
+
|
|
115
|
+
Two situations need a different value:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# Your site *is* the popup (you are the identity provider): the page the client
|
|
119
|
+
# opens must keep window.opener, which requires disabling isolation on it.
|
|
120
|
+
config.middleware.use HeaderGuard::Middleware, "Cross-Origin-Opener-Policy" => "unsafe-none"
|
|
121
|
+
|
|
122
|
+
# You open no popups and want the strictest isolation available (also the
|
|
123
|
+
# value required, together with COEP, for cross-origin isolated features such
|
|
124
|
+
# as SharedArrayBuffer):
|
|
125
|
+
config.middleware.use HeaderGuard::Middleware, "Cross-Origin-Opener-Policy" => "same-origin"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**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
129
|
#### 2\. Custom Content Security Policy (CSP)
|
|
102
130
|
|
|
103
|
-
|
|
131
|
+
The default CSP is a strict same-origin baseline:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
default-src 'self'; base-uri 'self'; font-src 'self'; form-action 'self';
|
|
135
|
+
frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self';
|
|
136
|
+
upgrade-insecure-requests
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
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
140
|
|
|
105
141
|
```ruby
|
|
106
|
-
custom_csp = "default-src 'self'; script-src 'self'
|
|
142
|
+
custom_csp = "default-src 'self'; script-src 'self' https://trusted.cdn.com;"
|
|
107
143
|
use HeaderGuard::Middleware, content_security_policy: custom_csp
|
|
108
144
|
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
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:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Allowing inline styles, explicitly.
|
|
151
|
+
use HeaderGuard::Middleware,
|
|
152
|
+
content_security_policy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'"
|
|
153
|
+
|
|
109
154
|
```
|
|
110
155
|
#### 3\. Report-Only Mode
|
|
111
156
|
|
|
@@ -116,18 +161,29 @@ To test a new CSP without enforcing it, set the report\_only option to true. Thi
|
|
|
116
161
|
config.middleware.use HeaderGuard::Middleware, report_only: true
|
|
117
162
|
|
|
118
163
|
```
|
|
164
|
+
#### 4\. Restricting to HTML Responses (legacy behaviour)
|
|
165
|
+
|
|
166
|
+
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`:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# 0.1.x behaviour: inject only on 2xx text/html responses.
|
|
170
|
+
config.middleware.use HeaderGuard::Middleware, html_only: true
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
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.
|
|
119
174
|
|
|
120
175
|
How It Works
|
|
121
176
|
------------
|
|
122
177
|
|
|
123
|
-
HeaderGuard hooks into the Rack request lifecycle and
|
|
178
|
+
HeaderGuard hooks into the Rack request lifecycle and, on every response passing through it:
|
|
124
179
|
|
|
125
180
|
1. **Header Merging:** It takes the default security headers and merges them with any custom headers supplied during initialization, ensuring user configuration takes precedence.
|
|
126
181
|
|
|
127
|
-
2. **Injection:** It injects the
|
|
182
|
+
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
183
|
|
|
129
|
-
3. **CSP Injection:** It injects the configured Content Security Policy
|
|
184
|
+
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
185
|
|
|
186
|
+
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
187
|
|
|
132
188
|
Development
|
|
133
189
|
-----------
|
|
@@ -7,7 +7,24 @@ 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 or
|
|
15
|
+
# content type. HSTS matters most on the HTTP->HTTPS redirect, and nosniff
|
|
16
|
+
# 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
|
+
# Passing `html_only: true` restores the 0.1.x behaviour, where nothing was
|
|
23
|
+
# injected unless the response was a 2xx with an HTML content type.
|
|
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
|
+
|
|
11
28
|
# Initializes the middleware. It merges user-defined options over defaults.
|
|
12
29
|
#
|
|
13
30
|
# @param app [Object] The next application in the Rack stack.
|
|
@@ -21,6 +38,7 @@ module HeaderGuard
|
|
|
21
38
|
# Extract special configuration settings
|
|
22
39
|
custom_csp = config.delete(:content_security_policy)
|
|
23
40
|
@report_only = config.delete(:report_only) || false
|
|
41
|
+
@html_only = config.delete(:html_only) || false
|
|
24
42
|
|
|
25
43
|
# 1. Start with DEFAULT_HEADERS (from header_guard.rb)
|
|
26
44
|
# 2. Merge remaining options (which are custom headers) over the defaults,
|
|
@@ -40,32 +58,54 @@ module HeaderGuard
|
|
|
40
58
|
def call(env)
|
|
41
59
|
status, headers, body = @app.call(env)
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if
|
|
46
|
-
# Inject the standard headers
|
|
61
|
+
html = html?(headers)
|
|
62
|
+
|
|
63
|
+
if apply_standard_headers?(status, html)
|
|
47
64
|
@headers.each do |key, value|
|
|
48
65
|
# We use assignment here, not `||=`, to ensure the middleware
|
|
49
66
|
# overwrites any headers set by the application before it,
|
|
50
67
|
# adhering to the strong security posture.
|
|
51
68
|
assign(headers, key, value)
|
|
52
69
|
end
|
|
53
|
-
|
|
54
|
-
# Inject the configured CSP header
|
|
55
|
-
assign(headers, @csp_header_key, @csp_value)
|
|
56
70
|
end
|
|
57
71
|
|
|
72
|
+
assign(headers, @csp_header_key, @csp_value) if apply_csp?(status, html)
|
|
73
|
+
|
|
58
74
|
[status, headers, body]
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
private
|
|
62
78
|
|
|
79
|
+
# Standard headers go on every response. In html_only mode they are
|
|
80
|
+
# restricted to 2xx HTML, as in 0.1.x.
|
|
81
|
+
def apply_standard_headers?(status, html)
|
|
82
|
+
return true unless @html_only
|
|
83
|
+
|
|
84
|
+
success?(status) && html
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# CSP goes on every HTML response regardless of status. In html_only mode
|
|
88
|
+
# it is restricted to 2xx HTML, as in 0.1.x.
|
|
89
|
+
def apply_csp?(status, html)
|
|
90
|
+
return false unless html
|
|
91
|
+
return true unless @html_only
|
|
92
|
+
|
|
93
|
+
success?(status)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def success?(status)
|
|
97
|
+
(200..299).cover?(status)
|
|
98
|
+
end
|
|
99
|
+
|
|
63
100
|
def normalize_key(key)
|
|
64
101
|
key.to_s.downcase
|
|
65
102
|
end
|
|
66
103
|
|
|
67
104
|
def html?(headers)
|
|
68
|
-
fetch_header(headers, "content-type")
|
|
105
|
+
content_type = fetch_header(headers, "content-type")
|
|
106
|
+
return false unless content_type
|
|
107
|
+
|
|
108
|
+
HTML_CONTENT_TYPES.any? { |type| content_type.include?(type) }
|
|
69
109
|
end
|
|
70
110
|
|
|
71
111
|
# 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.2.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
|