header_guard 0.1.0 → 0.1.2
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 +70 -0
- data/Gemfile.lock +2 -2
- data/LICENSE.txt +21 -0
- data/header_guard.gemspec +7 -3
- data/lib/header_guard/middleware.rb +52 -12
- data/lib/header_guard/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af8346262e02cbf08a1efb6b7de1ea08d55f8d262be2801421a8f848796b96e4
|
|
4
|
+
data.tar.gz: f8a9d346ca8ba26ae507d99d1780a97be8499e9e5e537239de32fee4fb043ed8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b08e3c213d232cb27d04eb8714f71894b9dfd9b0543d02ed971d9171551ecde1d57b2b57a938019facf9c5d69f568096453c7aa20b1a347e3067f2734dcc20b8
|
|
7
|
+
data.tar.gz: bb04592f757450600bae59a35b2572253642af6aa69af37e8c4e6724657b22d294a5aa6d859c39a2354262f42d13a9883be92a4b63d83d7b11321eb186578e7c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
See `PLAN.md` for the remaining remediation work (P1–P5), targeted at 0.2.0.
|
|
11
|
+
|
|
12
|
+
## [0.1.2] - 2026-09-08
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Security headers were silently not injected on conformant Rack 3 applications.**
|
|
17
|
+
The HTML check read `headers["Content-Type"]`, but the Rack 3 SPEC requires response
|
|
18
|
+
header keys to be lowercase. An application returning a plain Hash such as
|
|
19
|
+
`{"content-type" => "text/html"}` received **no** security headers at all — no HSTS,
|
|
20
|
+
no CSP, no `nosniff` — with no error raised. The gem only appeared to work for
|
|
21
|
+
applications built on `Rack::Response`, which returns a case-insensitive
|
|
22
|
+
`Rack::Headers`. Header lookup is now case-insensitive, so both Rack 2 and Rack 3
|
|
23
|
+
style responses are detected.
|
|
24
|
+
|
|
25
|
+
- **Emitted header names violated the Rack 3 SPEC.** Headers were written with
|
|
26
|
+
capitalized keys (`Strict-Transport-Security`), which `Rack::Lint` rejects with
|
|
27
|
+
`uppercase character in header name`. All injected headers are now written in
|
|
28
|
+
lowercase.
|
|
29
|
+
|
|
30
|
+
- **A header set by the application under a different capitalization is no longer
|
|
31
|
+
duplicated.** Previously an app setting `X-Frame-Options` would end up with both its
|
|
32
|
+
own key and the middleware's, sending the header twice. Differently-cased duplicates
|
|
33
|
+
are removed before the middleware writes its value.
|
|
34
|
+
|
|
35
|
+
- **A custom header option now overrides the matching default regardless of case.**
|
|
36
|
+
Passing `"x-frame-options" => "SAMEORIGIN"` previously appended a second header
|
|
37
|
+
instead of replacing the default.
|
|
38
|
+
|
|
39
|
+
- **Previously released `.gem` files are no longer packaged inside new releases.**
|
|
40
|
+
`header_guard-0.1.0.gem` and `header_guard-0.1.1.gem` were tracked in git, and the
|
|
41
|
+
gemspec filtered only the *current* version's file, so each release bundled every
|
|
42
|
+
earlier one — 0.1.1 was 16KB largely because it contained 0.1.0. The artifacts are now
|
|
43
|
+
untracked and gitignored, and the gemspec rejects any `.gem` file rather than one
|
|
44
|
+
specific name. `PLAN.md` is excluded from the package as well.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Injected response header names are now lowercase. This is invisible over HTTP, where
|
|
49
|
+
header names are case-insensitive, but code inspecting the raw Rack headers Hash by an
|
|
50
|
+
exact capitalized key needs to be updated. `Rack::Headers` and `Rack::Test` lookups are
|
|
51
|
+
unaffected.
|
|
52
|
+
|
|
53
|
+
## [0.1.1] - 2025-10-20
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- Corrected the `homepage` metadata in the gemspec.
|
|
58
|
+
|
|
59
|
+
## [0.1.0] - 2025-10-20
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- Initial release: Rack middleware injecting HSTS, `X-Content-Type-Options`,
|
|
64
|
+
`X-Frame-Options`, `Referrer-Policy` and a configurable Content Security Policy,
|
|
65
|
+
with `report_only` support.
|
|
66
|
+
|
|
67
|
+
[Unreleased]: https://github.com/danielefrisanco/headerguard/compare/v0.1.2...HEAD
|
|
68
|
+
[0.1.2]: https://github.com/danielefrisanco/headerguard/compare/v0.1.1...v0.1.2
|
|
69
|
+
[0.1.1]: https://github.com/danielefrisanco/headerguard/compare/v0.1.0...v0.1.1
|
|
70
|
+
[0.1.0]: https://github.com/danielefrisanco/headerguard/releases/tag/v0.1.0
|
data/Gemfile.lock
CHANGED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daniele Frisanco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/header_guard.gemspec
CHANGED
|
@@ -10,13 +10,17 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
|
|
11
11
|
spec.summary = "A robust Rack middleware for enforcing modern HTTP security headers, including a highly configurable Content Security Policy (CSP)."
|
|
12
12
|
spec.description = "Designed for applications that require strong browser-side security, HeaderGuard automatically injects HSTS, X-Content-Type-Options, X-Frame-Options, and a customizable CSP. Ideal for SSO and high-security web services."
|
|
13
|
-
spec.homepage = "https://github.com/
|
|
13
|
+
spec.homepage = "https://github.com/danielefrisanco/headerguard"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">= 2.6.6"
|
|
15
|
+
spec.required_ruby_version = ">= 2.6.6"
|
|
16
16
|
# Specify which files should be added to the gem when it is released.
|
|
17
17
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
18
18
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
19
|
-
|
|
19
|
+
# Never package build artifacts or internal planning docs. Matching any
|
|
20
|
+
# ".gem" (not just the current version's) keeps a stray local build from
|
|
21
|
+
# being bundled into a release.
|
|
22
|
+
f.end_with?(".gem") ||
|
|
23
|
+
f == "PLAN.md" ||
|
|
20
24
|
f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
21
25
|
end
|
|
22
26
|
end
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module HeaderGuard
|
|
4
4
|
# The Rack middleware class responsible for injecting security headers.
|
|
5
|
+
#
|
|
6
|
+
# Header names are treated case-insensitively throughout. The Rack 3 SPEC
|
|
7
|
+
# requires response header keys to be lowercase, while Rack 2 applications
|
|
8
|
+
# conventionally use capitalized keys, so this middleware reads incoming
|
|
9
|
+
# headers case-insensitively and always writes lowercase keys.
|
|
5
10
|
class Middleware
|
|
6
11
|
# Initializes the middleware. It merges user-defined options over defaults.
|
|
7
12
|
#
|
|
@@ -9,7 +14,7 @@ module HeaderGuard
|
|
|
9
14
|
# @param options [Hash] Configuration options for headers and CSP.
|
|
10
15
|
def initialize(app, options = {})
|
|
11
16
|
@app = app
|
|
12
|
-
|
|
17
|
+
|
|
13
18
|
# Use a copy of options for configuration extraction
|
|
14
19
|
config = options.dup
|
|
15
20
|
|
|
@@ -18,14 +23,17 @@ module HeaderGuard
|
|
|
18
23
|
@report_only = config.delete(:report_only) || false
|
|
19
24
|
|
|
20
25
|
# 1. Start with DEFAULT_HEADERS (from header_guard.rb)
|
|
21
|
-
# 2. Merge remaining options (which are custom headers) over the defaults
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
26
|
+
# 2. Merge remaining options (which are custom headers) over the defaults,
|
|
27
|
+
# so the user's header values take precedence.
|
|
28
|
+
# 3. Normalize every key to lowercase, so a custom "X-Frame-Options"
|
|
29
|
+
# overrides the default rather than being emitted alongside it.
|
|
30
|
+
@headers = DEFAULT_HEADERS.merge(config).each_with_object({}) do |(key, value), normalized|
|
|
31
|
+
normalized[normalize_key(key)] = value
|
|
32
|
+
end.freeze
|
|
25
33
|
|
|
26
34
|
# Set the final CSP value and the header key based on report_only setting
|
|
27
35
|
@csp_value = custom_csp || DEFAULT_CSP
|
|
28
|
-
@csp_header_key = @report_only ? "
|
|
36
|
+
@csp_header_key = @report_only ? "content-security-policy-report-only" : "content-security-policy"
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
# The Rack application call method.
|
|
@@ -34,21 +42,53 @@ module HeaderGuard
|
|
|
34
42
|
|
|
35
43
|
# Only inject headers on successful (2xx) responses with HTML content.
|
|
36
44
|
# Exclude redirects, errors, and non-HTML assets (like JSON or images).
|
|
37
|
-
if (200..299).include?(status) &&
|
|
38
|
-
|
|
45
|
+
if (200..299).include?(status) && html?(headers)
|
|
39
46
|
# Inject the standard headers
|
|
40
47
|
@headers.each do |key, value|
|
|
41
|
-
# We use assignment
|
|
48
|
+
# We use assignment here, not `||=`, to ensure the middleware
|
|
42
49
|
# overwrites any headers set by the application before it,
|
|
43
50
|
# adhering to the strong security posture.
|
|
44
|
-
headers
|
|
51
|
+
assign(headers, key, value)
|
|
45
52
|
end
|
|
46
|
-
|
|
53
|
+
|
|
47
54
|
# Inject the configured CSP header
|
|
48
|
-
headers
|
|
55
|
+
assign(headers, @csp_header_key, @csp_value)
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
[status, headers, body]
|
|
52
59
|
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def normalize_key(key)
|
|
64
|
+
key.to_s.downcase
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def html?(headers)
|
|
68
|
+
fetch_header(headers, "content-type")&.include?("text/html")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Rack 3 responses key headers in lowercase; Rack 2 applications typically
|
|
72
|
+
# send "Content-Type". A Rack::Headers hash resolves either directly, but a
|
|
73
|
+
# plain Hash does not, so fall back to scanning for a case-insensitive match.
|
|
74
|
+
def fetch_header(headers, key)
|
|
75
|
+
return headers[key] if headers.key?(key)
|
|
76
|
+
|
|
77
|
+
match = headers.keys.find { |candidate| normalize_key(candidate) == key }
|
|
78
|
+
match && headers[match]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Writes the lowercase key, first removing any differently-cased duplicate
|
|
82
|
+
# the application may have set, so the response never carries the same
|
|
83
|
+
# header twice under two spellings.
|
|
84
|
+
def assign(headers, key, value)
|
|
85
|
+
headers.keys.each do |existing|
|
|
86
|
+
next if existing == key
|
|
87
|
+
|
|
88
|
+
headers.delete(existing) if normalize_key(existing) == key
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
headers[key] = value
|
|
92
|
+
end
|
|
53
93
|
end
|
|
54
94
|
end
|
data/lib/header_guard/version.rb
CHANGED
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.1.
|
|
4
|
+
version: 0.1.2
|
|
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:
|
|
12
|
+
date: 2026-09-08 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rack
|
|
@@ -62,14 +62,16 @@ executables: []
|
|
|
62
62
|
extensions: []
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
|
+
- CHANGELOG.md
|
|
65
66
|
- Gemfile
|
|
66
67
|
- Gemfile.lock
|
|
68
|
+
- LICENSE.txt
|
|
67
69
|
- README.md
|
|
68
70
|
- header_guard.gemspec
|
|
69
71
|
- lib/header_guard.rb
|
|
70
72
|
- lib/header_guard/middleware.rb
|
|
71
73
|
- lib/header_guard/version.rb
|
|
72
|
-
homepage: https://github.com/
|
|
74
|
+
homepage: https://github.com/danielefrisanco/headerguard
|
|
73
75
|
licenses:
|
|
74
76
|
- MIT
|
|
75
77
|
metadata: {}
|
|
@@ -88,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
90
|
- !ruby/object:Gem::Version
|
|
89
91
|
version: '0'
|
|
90
92
|
requirements: []
|
|
91
|
-
rubygems_version: 3.
|
|
93
|
+
rubygems_version: 3.3.26
|
|
92
94
|
signing_key:
|
|
93
95
|
specification_version: 4
|
|
94
96
|
summary: A robust Rack middleware for enforcing modern HTTP security headers, including
|