From 4478fa5a37c2b64984b1a928e42fd5ef1bd6c7ea Mon Sep 17 00:00:00 2001 From: Lawson Jaglom-Kurtz Date: Fri, 26 Jul 2024 15:30:36 -0600 Subject: [PATCH] Enforce protocol allowlisting for `image` and `image-set` CSS funcs https://drafts.csswg.org/css-images-4/ introduces `url()`-like functionality in new `image` and `image-set` functions. This commit adds enforcement of protocol allowlisting for these functions. Note that `image-set` is already widely supported, whereas `image` is not yet. --- lib/sanitize/css.rb | 27 +++++++++++++++++++++++++++ test/test_sanitize_css.rb | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/lib/sanitize/css.rb b/lib/sanitize/css.rb index c570a26..2741e76 100644 --- a/lib/sanitize/css.rb +++ b/lib/sanitize/css.rb @@ -272,6 +272,10 @@ def property!(prop) return nil unless valid_url?(child) end + if name == 'image-set' || name == 'image' + return nil unless valid_image?(child) + end + combined_value << name return nil if name == 'expression' || combined_value == 'expression' end @@ -345,4 +349,27 @@ def valid_url?(node) false end + # Returns `true` if the given node (which is an `image` or `image-set` function) contains only strings + # using an allowlisted protocol. + def valid_image?(node) + return false unless node[:node] == :function + return false unless node.key?(:name) && ['image', 'image-set'].include?(node[:name].downcase) + return false unless Array === node[:value] + + node[:value].each do |token| + return false unless Hash === token + + case token[:node] + when :string + if token[:value] =~ Sanitize::REGEX_PROTOCOL + return false unless @config[:protocols].include?($1.downcase) + else + return false unless @config[:protocols].include?(:relative) + end + else + next + end + end + end + end; end diff --git a/test/test_sanitize_css.rb b/test/test_sanitize_css.rb index 185b650..46df688 100644 --- a/test/test_sanitize_css.rb +++ b/test/test_sanitize_css.rb @@ -29,6 +29,12 @@ "background: url('ht\\tp://example.com/http.jpg')", "background: url(https://example.com/https.jpg)", "background: url('https://example.com/https.jpg')", + "background: image-set('relative.jpg' 1x, 'relative-2x.jpg' 2x)", + "background: image-set('https://example.com/https.jpg' 1x, 'https://example.com/https-2x.jpg' 2x)", + "background: image-set('https://example.com/https.jpg' type('image/jpeg'), 'https://example.com/https.avif' type('image/avif'))", + "background: image('relative.jpg');", + "background: image('https://example.com/https.jpg');", + "background: image(rtl 'https://example.com/https.jpg');" ].each do |css| _(@default.properties(css)).must_equal '' _(@relaxed.properties(css)).must_equal css