Skip to content

Add Subresource Integrity (SRI) verification for remote stylesheets - #190

Open
JLLeitschuh wants to merge 3 commits into
premailer:masterfrom
JLLeitschuh:feature/subresource-integrity
Open

Add Subresource Integrity (SRI) verification for remote stylesheets#190
JLLeitschuh wants to merge 3 commits into
premailer:masterfrom
JLLeitschuh:feature/subresource-integrity

Conversation

@JLLeitschuh

@JLLeitschuh JLLeitschuh commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

Adds an integrity: option to Parser#load_uri! that verifies a fetched remote stylesheet against a Subresource Integrity value before it's parsed — the same value an HTML <link integrity="..."> attribute carries.

parser.load_uri!(
  'http://example.com/styles/style.css',
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC'
)
  • Supports sha256/sha384/sha512, multiple space-separated values in one integrity: string (exactly like the HTML attribute), and the spec's "agility" rule: when a value lists more than one algorithm, only the strongest present one is actually checked, and every weaker one is ignored outright — e.g. sha256-... sha384-... only checks the sha384 value. When several values are given for that same strongest algorithm (e.g. during a planned key/stylesheet rotation), matching any one of them is enough.
  • A value naming only an algorithm this library doesn't recognize is treated as unverifiable (passes through) rather than failing every such fetch.
  • On mismatch, the fetch fails the same way other remote-fetch failures already do here: raises CssParser::RemoteFileError when io_exceptions is enabled, otherwise loads nothing.

Why

Consumers of this gem that already know the expected digest of a linked stylesheet (for example, because it came from an HTML <link integrity="..."> attribute they're processing) currently have no way to ask load_uri! to verify it — the fetched body is trusted unconditionally regardless of what the caller expected. This gives callers an opt-in way to fail closed instead.

Testing

  • New test/test_css_parser_integrity.rb, covering: normal pass-through when the option is omitted, all three supported algorithms, mismatch handling (with and without io_exceptions), the multi-algorithm "agility" rule in both directions (weaker-correct+stronger-wrong fails, weaker-wrong+stronger-correct passes), multiple acceptable values for one algorithm, and the unsupported-algorithm-is-unverifiable case.
  • Full existing suite passes (bundle exec rake test) — no behavior change when integrity: isn't passed.
  • bundle exec rubocop clean on the changed files.

Happy to adjust the option name/shape or add doc updates elsewhere if you'd rather it live somewhere other than load_uri!.

JLLeitschuh and others added 2 commits September 1, 2026 14:48
Adds an `integrity:` option to `Parser#load_uri!` that verifies a fetched
remote stylesheet against a Subresource Integrity value
(https://www.w3.org/TR/SRI/) before it is parsed, mirroring the
`integrity` attribute browsers already support on `<link>`/`<script>`
tags. Supports sha256/sha384/sha512, multiple space-separated values,
and the SRI "agility" rule (only the strongest present algorithm is
checked). A value naming only an unrecognized algorithm is treated as
unverifiable rather than failing the fetch.

On mismatch, the fetch fails the same way other remote-fetch failures
already do: raises CssParser::RemoteFileError when io_exceptions is
enabled, otherwise loads nothing. Useful for any caller that already
knows the expected digest of a linked stylesheet and wants a stale or
unexpectedly-changed response to fail closed rather than be silently
parsed and applied.

Co-authored-by: Claude Sonnet 5 <[email protected]>
Ruby 3.4+ removed `base64` from default gems (it's now a bundled gem
that must be an explicit dependency to be resolvable). `require 'base64'`
in parser.rb was relying on it still being present by default, which
breaks under Ruby 4.0 CI with a LoadError. Declare it in the gemspec.

Co-authored-by: Claude Sonnet 5 <[email protected]>
@JLLeitschuh

Copy link
Copy Markdown
Contributor Author

Fixed — Ruby 3.4+/4.0 removed base64 from default gems, so require 'base64' needs it declared as an explicit runtime dependency. Added s.add_dependency 'base64' and updated Gemfile.lock accordingly. Full suite + rubocop pass locally on 3.3.9; should now pass on the 4.0 CI job too.

@JLLeitschuh

Copy link
Copy Markdown
Contributor Author

Re: the JRuby job failure (CssParserOffsetCaptureTests#test_capturing_offsets_from_remote_file, RemoteFileError: https://dialect.ca/inc/screen.css) — that's pre-existing and unrelated to this PR:

  • It reproduces identically against unmodified master (I hit the same error before making any changes here).
  • The test never passes the new integrity: option, so this PR's new code path isn't reached by it at all.
  • The test itself is unchanged since 2021 (3a3b21e) and makes a live network call to a real third-party site — dialect.ca/inc/screen.css is reachable right now, so it's not permanently dead, just flaky in CI (possibly a JRuby/JVM TLS-stack quirk against that host, since JRuby uses its own TLS implementation rather than MRI's OpenSSL, per this repo's "Drop jruby-openssl dependency" history).

Flagging so it's not mistaken for a regression from this change.

Adds a second README example showing the multi-value/multi-algorithm
form of the `integrity:` option (as SRI itself allows), since the
existing example only showed a single sha384 value and didn't make
the "strongest algorithm wins" behavior visible without reading the
implementation.

Co-authored-by: Claude Sonnet 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant