header_guard 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1b0c70ef3860e8eb858d0f1788b6f77693dd832c49a26f4a6d7d30a8e16989a
4
- data.tar.gz: b7938f96c5900e6fa4f7f80034baf438277b9759592d5136c072d0de94630b79
3
+ metadata.gz: af8346262e02cbf08a1efb6b7de1ea08d55f8d262be2801421a8f848796b96e4
4
+ data.tar.gz: f8a9d346ca8ba26ae507d99d1780a97be8499e9e5e537239de32fee4fb043ed8
5
5
  SHA512:
6
- metadata.gz: e38ca2186bcfe23bcd7fce5b6c48d660f007bef259f36ff527858c43a742fdc60bd9ed417a0821d2852a3ea840c4688e5a621265579ad85ae3dda6de87630342
7
- data.tar.gz: fceb2b577b294e6d026ddda01416b4c09ef48e88b86db274242f68993f72882da8e1ad9b60fe5bbec24aab29ae4776049cd90e35d11df0eb50e97add9d76c9c6
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- header_guard (0.1.1)
4
+ header_guard (0.1.2)
5
5
  rack (~> 3.0)
6
6
 
7
7
  GEM
@@ -35,4 +35,4 @@ DEPENDENCIES
35
35
  rspec (~> 3.12)
36
36
 
37
37
  BUNDLED WITH
38
- 2.4.22
38
+ 2.4.9
data/header_guard.gemspec CHANGED
@@ -12,11 +12,15 @@ Gem::Specification.new do |spec|
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
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
- (f == spec.full_name + ".gem") ||
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
- # THIS IS THE FIX: By merging 'config' (which now only contains headers)
23
- # over the defaults, the user's header values take precedence.
24
- @headers = DEFAULT_HEADERS.merge(config).freeze
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 ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"
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) && headers["Content-Type"]&.include?("text/html")
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 (=) here, not `||=`, to ensure the middleware
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[key] = value
51
+ assign(headers, key, value)
45
52
  end
46
-
53
+
47
54
  # Inject the configured CSP header
48
- headers[@csp_header_key] = @csp_value
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module HeaderGuard
4
4
  # The current version of the HeaderGuard gem.
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  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.1.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: 2025-10-20 00:00:00.000000000 Z
12
+ date: 2026-09-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -62,11 +62,11 @@ executables: []
62
62
  extensions: []
63
63
  extra_rdoc_files: []
64
64
  files:
65
+ - CHANGELOG.md
65
66
  - Gemfile
66
67
  - Gemfile.lock
67
68
  - LICENSE.txt
68
69
  - README.md
69
- - header_guard-0.1.0.gem
70
70
  - header_guard.gemspec
71
71
  - lib/header_guard.rb
72
72
  - lib/header_guard/middleware.rb
@@ -90,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
90
  - !ruby/object:Gem::Version
91
91
  version: '0'
92
92
  requirements: []
93
- rubygems_version: 3.2.3
93
+ rubygems_version: 3.3.26
94
94
  signing_key:
95
95
  specification_version: 4
96
96
  summary: A robust Rack middleware for enforcing modern HTTP security headers, including
Binary file