-
Notifications
You must be signed in to change notification settings - Fork 0
Recognize more base64 URIs to match Chrome behavior #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8f8fe65
db91c04
3d32c04
ba53216
14a2bfb
e733e38
7422cc6
05f6ce2
c762bf2
b2ecfd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| require "base64" | ||
| require "json" | ||
|
|
||
| class ::DataUri | ||
| REGEXP = %r{ | ||
| \Adata: | ||
|
|
@@ -10,16 +13,30 @@ class ::DataUri | |
| (?<data>.*) | ||
| }x.freeze | ||
|
|
||
| attr_reader :uri, :match | ||
| attr_reader :mimetype, :parameters, :extension, :data, :uri, :match | ||
|
|
||
| def initialize(uri) | ||
| match = REGEXP.match(uri) | ||
| raise ArgumentError, 'invalid data URI' unless match | ||
|
|
||
| @uri = uri | ||
| @match = match | ||
| @match = REGEXP.match(uri) | ||
| raise ArgumentError, 'invalid data URI' unless @match | ||
|
|
||
| header_end = @match.begin(:data) | ||
| header = header_end.nil? ? '' : uri[0...header_end] | ||
| @header_contains_base64 = !!(header.match?(/base64/i)) | ||
|
|
||
| @mimetype = String(@match[:mimetype]).empty? ? 'text/plain' : @match[:mimetype] | ||
| @parameters = String(@match[:parameters]).split(';').reject(&:empty?) | ||
| @extension = @match[:extension] | ||
| @data = @match[:data] | ||
|
|
||
| validate_base64_content | ||
|
|
||
| if uri.start_with?('data:,') | ||
| @mimetype = 'text/plain' | ||
| @parameters = [] | ||
| @extension = nil | ||
| @data = uri.split(',', 2).last || '' | ||
| end | ||
| end | ||
|
|
||
| def self.valid?(uri) | ||
|
|
@@ -33,57 +50,62 @@ def self.valid?(uri) | |
|
|
||
| def self.esip6?(uri) | ||
| begin | ||
| parameters = DataUri.new(uri).parameters | ||
| data_uri = DataUri.new(uri) | ||
|
|
||
| parameters.include?("rule=esip6") | ||
| # Use legacy behavior to support Ethscriptions | ||
| raw_parameters = String(data_uri.match[:parameters]).split(';') | ||
| raw_parameters.include?("rule=esip6") | ||
| rescue ArgumentError | ||
| false | ||
| end | ||
| end | ||
|
|
||
| def validate_base64_content | ||
| if base64? | ||
| begin | ||
| Base64.strict_decode64(data) | ||
| rescue ArgumentError | ||
| raise ArgumentError, 'malformed base64 content' | ||
| end | ||
| end | ||
| return unless claims_to_be_base64? | ||
|
|
||
| raise ArgumentError, 'malformed base64 content' unless data_valid_base64? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Base64 validation misses implicit claims.The validation in |
||
| end | ||
|
|
||
| def mediatype | ||
| "#{mimetype}#{parameters}" | ||
| end | ||
|
|
||
| def base64? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Mediatype Output Incorrectly Includes Parameters ArrayThe |
||
| @header_contains_base64 && data_valid_base64? | ||
| end | ||
|
|
||
| def decoded_data | ||
| return data unless base64? | ||
|
|
||
| Base64.decode64(data) | ||
| base64? ? base64_decoded_data : data | ||
| end | ||
|
|
||
| def base64? | ||
| def claims_to_be_base64? | ||
| !String(extension).empty? | ||
| end | ||
|
|
||
| def mimetype | ||
| if String(match[:mimetype]).empty? || uri.starts_with?("data:,") | ||
| return 'text/plain' | ||
| end | ||
| def data_valid_base64? | ||
| !base64_decoded_data.nil? | ||
| end | ||
RogerPodacter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def json? | ||
| return @_json if instance_variable_defined?(:@_json) | ||
|
|
||
| match[:mimetype] | ||
| @_json = mimetype.include?('json') || decoded_data.lstrip.start_with?('{', '[') | ||
| end | ||
|
|
||
| def data | ||
| match[:data] | ||
| def parse_json(symbolize_names: false, max_nesting: 100) | ||
| raise ArgumentError, 'not JSON' unless json? | ||
| JSON.parse(decoded_data, symbolize_names: symbolize_names, max_nesting: max_nesting) | ||
| end | ||
|
|
||
| def parameters | ||
| return [] if String(match[:mimetype]).empty? && String(match[:parameters]).empty? | ||
|
|
||
| match[:parameters].split(";").reject(&:empty?) | ||
| end | ||
|
|
||
| def extension | ||
| match[:extension] | ||
| private | ||
|
|
||
| def base64_decoded_data | ||
| return @base64_decoded_data if instance_variable_defined?(:@base64_decoded_data) | ||
|
|
||
| @base64_decoded_data = begin | ||
| Base64.strict_decode64(data) | ||
| rescue ArgumentError | ||
| nil | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "minitest/autorun" | ||
| require "facet_rails_common/data_uri" | ||
|
|
||
| class DataUriTest < Minitest::Test | ||
| def test_decoded_data_with_base64_extension | ||
| uri = "data:text/plain;base64,SGVsbG8=" | ||
|
|
||
| assert_equal "Hello", DataUri.new(uri).decoded_data | ||
| end | ||
|
|
||
| def test_implicit_data_uri_keeps_commas_after_first_split | ||
| du = DataUri.new("data:,hi/bye,yo") | ||
| assert_equal "hi/bye,yo", du.data | ||
| end | ||
|
|
||
| def test_implicit_data_uri_defaults_and_decoding | ||
| uri = "data:,Hello" | ||
| du = DataUri.new(uri) | ||
| assert_equal "text/plain", du.mimetype | ||
| assert_equal [], du.parameters | ||
| assert_nil du.extension | ||
| assert_equal "Hello", du.data | ||
| refute du.base64? | ||
| assert_equal "Hello", du.decoded_data | ||
| end | ||
|
|
||
| def test_decoded_data_when_metadata_mentions_base64 | ||
| uri = "data:text/plain;foo=base64,SGVsbG8=" | ||
|
|
||
| assert_equal "Hello", DataUri.new(uri).decoded_data | ||
| end | ||
|
|
||
| def test_decoded_data_when_metadata_mentions_base64_in_mime_type | ||
| uri = "data:application/Base64Something,SGVsbG8=" | ||
|
|
||
| assert_equal "Hello", DataUri.new(uri).decoded_data | ||
| end | ||
|
|
||
| def test_returns_original_data_when_metadata_mentions_base64_but_data_invalid | ||
| uri = "data:text/plain;foo=base64,@@@" | ||
| data_uri = DataUri.new(uri) | ||
|
|
||
| refute data_uri.data_valid_base64? | ||
| assert_equal "@@@", data_uri.decoded_data | ||
| end | ||
|
|
||
| def test_returns_original_data_when_base64_only_in_payload | ||
| uri = "data:text/plain,base64SGVsbG8=" | ||
|
|
||
| assert_equal "base64SGVsbG8=", DataUri.new(uri).decoded_data | ||
| end | ||
|
|
||
| def test_invalid_base64_with_extension_raises | ||
| uri = "data:text/plain;base64,@@@" | ||
|
|
||
| error = assert_raises(ArgumentError) { DataUri.new(uri) } | ||
| assert_equal "malformed base64 content", error.message | ||
| end | ||
|
|
||
| def test_implicit_form_with_legacy_base64_extension_raises | ||
| uri = "data:,text/plain;charset=utf-8;base64,8J+Msi50cmVl," | ||
|
|
||
| error = assert_raises(ArgumentError) { DataUri.new(uri) } | ||
| assert_equal "malformed base64 content", error.message | ||
| end | ||
|
|
||
| def test_implicit_form_with_legacy_base64_valid_decodes_and_defaults | ||
| uri = "data:,text/plain;charset=utf-8;base64,SGVsbG8=" | ||
| du = DataUri.new(uri) | ||
|
|
||
| assert_equal "text/plain", du.mimetype | ||
| assert_equal [], du.parameters | ||
| assert_nil du.extension | ||
| assert_equal "Hello", du.decoded_data | ||
| end | ||
|
|
||
| def test_valid_question_mark_false_for_implicit_legacy_invalid_base64 | ||
| uri = "data:,text/plain;charset=utf-8;base64,@@@" | ||
| refute DataUri.valid?(uri) | ||
| end | ||
|
|
||
| def test_uppercase_base64_param_valid_decodes | ||
| uri = "data:text/plain;foo=BASE64,SGVsbG8=" | ||
| du = DataUri.new(uri) | ||
|
|
||
| assert du.base64? | ||
| assert_equal "Hello", du.decoded_data | ||
| end | ||
|
|
||
| def test_uppercase_base64_param_invalid_returns_original | ||
| uri = "data:text/plain;foo=BASE64,@@@" | ||
| du = DataUri.new(uri) | ||
|
|
||
| refute du.data_valid_base64? | ||
| refute du.base64? | ||
| assert_equal "@@@", du.decoded_data | ||
| end | ||
|
|
||
| def test_empty_base64_content_decodes_to_empty_string | ||
| uri = "data:text/plain;base64," | ||
| du = DataUri.new(uri) | ||
|
|
||
| assert_equal "", du.decoded_data | ||
| end | ||
|
|
||
| def test_no_mimetype_with_base64_defaults_and_decodes | ||
| uri = "data:;base64,SGVsbG8=" | ||
| du = DataUri.new(uri) | ||
|
|
||
| assert_equal "text/plain", du.mimetype | ||
| assert_equal "Hello", du.decoded_data | ||
| end | ||
|
|
||
| def test_valid_question_mark_true_for_valid_data_uris | ||
| assert DataUri.valid?("data:text/plain,abc") | ||
| assert DataUri.valid?("data:,") | ||
| assert DataUri.valid?("data:application/json,{\"a\":1}") | ||
| end | ||
|
|
||
| def test_valid_question_mark_false_for_non_data_uris | ||
| refute DataUri.valid?("http://example.com") | ||
| refute DataUri.valid?("data;not,a,uri") | ||
| end | ||
|
|
||
| def test_json_helpers_by_mimetype | ||
| uri = "data:application/json,{\"a\":1}" | ||
| du = DataUri.new(uri) | ||
| assert du.json? | ||
| assert_equal({"a"=>1}, du.parse_json) | ||
| assert_equal({a: 1}, du.parse_json(symbolize_names: true)) | ||
| end | ||
|
|
||
| def test_json_helpers_by_payload_prefix | ||
| json_b64 = "eyJhIjoxfQ==" # {"a":1} | ||
| uri = "data:application/octet-stream;base64,#{json_b64}" | ||
| du = DataUri.new(uri) | ||
| assert du.json? | ||
| assert_equal({"a"=>1}, du.parse_json) | ||
| end | ||
|
|
||
| def test_parse_json_raises_when_not_json | ||
| du = DataUri.new("data:text/plain,hello") | ||
| error = assert_raises(ArgumentError) { du.parse_json } | ||
| assert_equal "not JSON", error.message | ||
| end | ||
|
|
||
| def test_esip6_param_detection | ||
| assert DataUri.esip6?("data:text/plain;rule=esip6,abc") | ||
| refute DataUri.esip6?("data:text/plain;rule=other,abc") | ||
| refute DataUri.esip6?("not a data uri") | ||
| end | ||
|
|
||
| def test_esip6_legacy_implicit_form | ||
| uri = 'data:,text/plain;rule=esip6,{"p":"erc-20","op":"mint","tick":"eths","amt":"1000"}' | ||
| assert DataUri.esip6?(uri) | ||
| end | ||
|
|
||
| def test_esip6_legacy_implicit_form_other_rule | ||
| uri = 'data:,text/plain;rule=other,{"p":"erc-20","op":"mint","tick":"eths","amt":"1000"}' | ||
| refute DataUri.esip6?(uri) | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: esip6? method fails for legacy implicit form URIs
The
esip6?method accessesmatch[:parameters]directly to check for therule=esip6parameter. However, for implicit form URIs likedata:,text/plain;rule=esip6,..., the regex captures everything afterdata:,as thedatagroup, leavingmatch[:parameters]empty. This causesesip6?to returnfalseeven whenrule=esip6appears in the content, causing thetest_esip6_legacy_implicit_formtest to fail.