css_parser 3.1.0 → 3.3.0

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: 7c675c0d95c9efce004a49e17cf09c880c44c4c11375f754c50586052c03202d
4
- data.tar.gz: 2d729de7e39e045d1e2594e4c847c495ef7765d34c37b76f53ef9348de0e1f96
3
+ metadata.gz: b1b890ee0d537d338ae0a218c68424271242859ba814a52c27fcd05ffefe1acd
4
+ data.tar.gz: 147e41dae4da802d96124fc07b488b122967a44c0ff79bfe623291c1795e0d9b
5
5
  SHA512:
6
- metadata.gz: ab3178e90c357ad4ede9c90ac70526bbe4b90a475690afa60551d4acfc2bb41a70db125bdda040a1bbcfa8f4e3dfa454401116aec745e639af9244c71c3f7c6a
7
- data.tar.gz: 15838f208ef82d6a2d5adbddcb5486ad5b13b0d88f32e3923b6f7bd064fbbe08dc4f6dc4af0a299a517ff7c4faec3010a044b59209b809248e58622b08a29c4a
6
+ metadata.gz: 76728b4b816f4b620f28198b6fd5f92f005733bec3afba0df5707d8daf99ab650fd0a0143a6222fc53c5453c32c3a13a76eb4877f3c06f5dc903a559a18d200f
7
+ data.tar.gz: a5b5a901312932dd9d3c50fa9a6cd4ea549180f57a7e4d85d700d5c3ec58f458927be66c2eb7169165333e153d58061dbdfa98c75c0e6ffb6a81181bc78fdd6f
@@ -173,6 +173,21 @@ module CssParser
173
173
  load_uri!(import_uri, import_options)
174
174
  elsif options[:base_dir]
175
175
  import_options[:base_dir] = options[:base_dir]
176
+ # The import path comes from (potentially untrusted) CSS
177
+ # content, not from the caller. Flag this load so load_file!
178
+ # keeps it contained within the import root and refuses
179
+ # absolute paths / `..` traversal that escape it -- otherwise
180
+ # @import turns into arbitrary local file disclosure. This is
181
+ # the base_dir sibling of the file://-via-base_uri hole closed
182
+ # in GHSA-9pmc-p236-855h, which left this arm untouched.
183
+ #
184
+ # import_root is the top-level base_dir, threaded through
185
+ # nested imports so containment is always checked against it.
186
+ # That still allows legitimate `../` imports that stay inside
187
+ # the tree (e.g. subdir/import2.css -> ../simple.css) while
188
+ # rejecting anything that leaves it.
189
+ import_options[:from_import] = true
190
+ import_options[:import_root] = options[:import_root] || options[:base_dir]
176
191
  load_file!(import_path, import_options)
177
192
  end
178
193
  end
@@ -558,8 +573,18 @@ module CssParser
558
573
  end
559
574
 
560
575
  # Load a local CSS file.
576
+ #
577
+ # +from_import+ (internal) marks a load whose +file_name+ originates
578
+ # from an @import rule in parsed CSS rather than from the caller. Such
579
+ # a path is attacker-influenced, so it is contained within the import
580
+ # root (+import_root+, the top-level +base_dir+): absolute paths and
581
+ # `..` traversal that escape the root are refused (raising
582
+ # RemoteFileError when io_exceptions are on, otherwise a silent no-op).
583
+ # Direct callers (the default, +from_import: false+) are unaffected --
584
+ # load_file! remains the explicit, trusted local-file API and may load
585
+ # any readable path.
561
586
  def load_file!(file_name, options = {}, deprecated = nil)
562
- opts = {base_dir: nil, media_types: :all}
587
+ opts = {base_dir: nil, media_types: :all, from_import: false, import_root: nil}
563
588
 
564
589
  if options.is_a? Hash
565
590
  opts.merge!(options)
@@ -570,7 +595,29 @@ module CssParser
570
595
  opts[:media_types] = deprecated if deprecated
571
596
  end
572
597
 
598
+ # Pin the containment root to the first base_dir seen in the chain.
599
+ # load_file! resets base_dir to each file's own directory as it
600
+ # recurses, so without this a file loaded from a subdirectory would
601
+ # shrink the sandbox and reject a legitimate `..` back toward the
602
+ # caller's original base_dir.
603
+ opts[:import_root] ||= opts[:base_dir]
604
+
573
605
  file_name = File.expand_path(file_name, opts[:base_dir])
606
+
607
+ # Containment check for @import-sourced paths: the resolved file must
608
+ # stay inside the import root. Rejects both `@import "/etc/passwd"`
609
+ # (absolute) and `@import "../../secret"` (traversal escaping the
610
+ # tree) from untrusted CSS, while leaving direct load_file! callers
611
+ # untouched and still permitting `..` that stays within the root.
612
+ if opts[:from_import] && (opts[:import_root] || opts[:base_dir])
613
+ root = File.expand_path((opts[:import_root] || opts[:base_dir]).to_s)
614
+ unless file_name == root || file_name.start_with?(root + File::SEPARATOR)
615
+ raise RemoteFileError, file_name if @options[:io_exceptions]
616
+
617
+ return
618
+ end
619
+ end
620
+
574
621
  return unless File.readable?(file_name)
575
622
  return unless circular_reference_check(file_name)
576
623
 
@@ -61,20 +61,27 @@ module CssParser
61
61
  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
62
62
 
63
63
  # Functions like calc, var, clamp, etc.
64
- RE_FUNCTIONS = /
65
- (
66
- [a-z0-9-]+ # function name
67
- )
68
- (?>
69
- \( # opening parenthesis
70
- (?:
71
- ([^()]+)
72
- | # recursion via subexpression
73
- \g<0>
74
- )*
75
- \) # closing parenthesis
76
- )
77
- /imx.freeze
64
+ # Possessive quantifiers + recursion-first alternation avoid exponential
65
+ # backtracking on unclosed functions, the timeout is a backstop.
66
+ RE_FUNCTIONS = Regexp.new(
67
+ /
68
+ (
69
+ [a-z0-9-]+ # function name
70
+ )
71
+ (?>
72
+ \( # opening parenthesis
73
+ (?:
74
+ \g<0> # nested function
75
+ |
76
+ [^()a-z0-9]++
77
+ |
78
+ [a-z0-9-]++ (?!\()
79
+ )*
80
+ \) # closing parenthesis
81
+ )
82
+ /imx,
83
+ timeout: 0.01
84
+ ).freeze
78
85
 
79
86
  # Patterns for specificity calculations
80
87
  NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC = /
@@ -696,10 +696,16 @@ module CssParser
696
696
  end
697
697
 
698
698
  def split_value_preserving_function_whitespace(value)
699
- split_value = value.gsub(RE_FUNCTIONS) do |c|
700
- c.gsub!(/\s+/, WHITESPACE_REPLACEMENT)
701
- c
702
- end
699
+ # hostile values can still hit the regexp timeout, then split without protecting functions
700
+ split_value =
701
+ begin
702
+ value.gsub(RE_FUNCTIONS) do |c|
703
+ c.gsub!(/\s+/, WHITESPACE_REPLACEMENT)
704
+ c
705
+ end
706
+ rescue Regexp::TimeoutError
707
+ value
708
+ end
703
709
 
704
710
  matches = split_value.strip.split(/\s+/)
705
711
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CssParser
4
- VERSION = '3.1.0'.freeze
4
+ VERSION = '3.3.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: css_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dunae