Add Subresource Integrity (SRI) verification for remote stylesheets - #190
Add Subresource Integrity (SRI) verification for remote stylesheets#190JLLeitschuh wants to merge 3 commits into
Conversation
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
|
Fixed — Ruby 3.4+/4.0 removed |
|
Re: the JRuby job failure (
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 <noreply@anthropic.com>
| # Subresource Integrity value -- a single `<algorithm>-<base64 digest>` token, or | ||
| # several whitespace-separated tokens (https://www.w3.org/TR/SRI/#the-integrity-attribute). | ||
| # Tokens using an algorithm this library doesn't recognize are ignored; per the spec's | ||
| # "agility" rule, when multiple recognized algorithms are present only the strongest one |
There was a problem hiding this comment.
why the strongest and not the first ?
| digest_class = { | ||
| 'sha512' => Digest::SHA512, | ||
| 'sha384' => Digest::SHA384, | ||
| 'sha256' => Digest::SHA256 | ||
| }.fetch(algorithm) |
There was a problem hiding this comment.
how about we make INTEGRITY_ALGORITHM_PRIORITY = {"sha512" => [0, Digest::SHA512], ...} so we don't have 2 different structures that need to be in sync
| return true if candidates.empty? | ||
|
|
||
| algorithm = candidates.map(&:first).min_by { |a| INTEGRITY_ALGORITHM_PRIORITY.index(a) } | ||
| expected_values = candidates.select { |a, _v| a == algorithm }.map { |_a, v| v } |
There was a problem hiding this comment.
multiple values for the same algorithm is expected ?
| # load a remote file, setting the base_uri and media_types | ||
| parser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', media_types: [:screen, :handheld]}) | ||
|
|
||
| # load a remote file, verifying it against a Subresource Integrity value |
There was a problem hiding this comment.
doubt many ppl will use this, so put it further at the back + in it's own section
| end | ||
|
|
||
| if integrity && !integrity_matches?(res.body, integrity) | ||
| raise RemoteFileError, uri.to_s if @options[:io_exceptions] |
There was a problem hiding this comment.
maybe new error that inherits from RemoteFileError ?
grosser
left a comment
There was a problem hiding this comment.
looks pretty good, a few nit comments/questions, otherwise 👍
What
Adds an
integrity:option toParser#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.sha256/sha384/sha512, multiple space-separated values in oneintegrity: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.CssParser::RemoteFileErrorwhenio_exceptionsis 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 askload_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
test/test_css_parser_integrity.rb, covering: normal pass-through when the option is omitted, all three supported algorithms, mismatch handling (with and withoutio_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.bundle exec rake test) — no behavior change whenintegrity:isn't passed.bundle exec rubocopclean 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!.