cataract 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +3 -2
- data/cataract.gemspec +2 -0
- data/lib/cataract/import_resolver.rb +42 -4
- data/lib/cataract/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2245bc15a5bd5029bcf6d0cb056a97a896d2855ef247dcc34a3764bba009e89
|
|
4
|
+
data.tar.gz: 170bd30c28c2f1c5405ea742a9398dad5aaba2115cd424c73e17572bed8399d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 329b054911204add270ac0f2853f0d79c0d12c0b549a34acb672fe6b71416b521f500b36403567f65afa620d165b980f6bc0068c661defe75226602b394c4791
|
|
7
|
+
data.tar.gz: b252e03c315de4885224f2bfdd9841ca94e7294b43a80c3c1e27a77d236d451b3ed1a779ad95d956a0445a90d6733d11c8d1ca45ad93d5a693304e7410673d72
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## [0.6.0] - 2026-07-30
|
|
2
|
+
|
|
3
|
+
- Security: `ImportResolver.validate_url`'s `dangerous_path_prefixes` check now canonicalizes the `file://` path (via `File.expand_path`) before comparing against blocked prefixes, instead of comparing the raw path string. Previously a path containing `..` that didn't literally start with a blocked prefix (e.g. `file:///var/../etc/passwd`) bypassed the check entirely, even though the file was actually read from the resolved location. Reachable via `Stylesheet#load_uri`/`#load_file` (file scheme allowed by default) and via `@import` when a caller opts `file` into `allowed_schemes`.
|
|
4
|
+
- Security/Breaking: `@import` and `Stylesheet#load_uri`/`#load_file` HTTP(S) fetches are now validated with the [ssrf_filter](https://rubygems.org/gems/ssrf_filter) gem, which blocks loopback, private-network, link-local, and cloud metadata addresses by default and re-validates on every redirect hop. Previously only URL scheme and file extension were checked, so an allowed `https://` URL (or a redirect target) could still reach an internal-only host. Set `allow_local_network: true` to restore the old, unfiltered behavior. Adds `ssrf_filter` as a new runtime dependency.
|
|
5
|
+
|
|
1
6
|
## [0.5.0] - 2026-07-09
|
|
2
7
|
|
|
3
8
|
- Feature: `@supports (condition) { ... }` now preserves its condition and the rules it wraps through parse -> serialize, in both backends - previously the condition was discarded entirely and the wrapped rules were silently flattened into the surrounding document with no trace of ever being conditional.
|
data/README.md
CHANGED
|
@@ -14,7 +14,6 @@ A performant CSS parser for accurate parsing of complex CSS structures.
|
|
|
14
14
|
- **CSS Color Level 4**: Parses and preserves modern color formats (hex, rgb, hsl, hwb, oklab, oklch, lab, lch, named colors). Optional color conversion utility for transforming between formats.
|
|
15
15
|
- **Specificity Calculation**: Automatic CSS specificity computation
|
|
16
16
|
- **Media Query Filtering**: Query rules by media type
|
|
17
|
-
- **Zero Runtime Dependencies**: Pure C extension with no runtime gem dependencies
|
|
18
17
|
|
|
19
18
|
## Installation
|
|
20
19
|
|
|
@@ -269,7 +268,8 @@ sheet = Cataract::Stylesheet.parse(css, import: {
|
|
|
269
268
|
extensions: ['css'], # Default: ['css']
|
|
270
269
|
max_depth: 3, # Default: 5
|
|
271
270
|
timeout: 10, # Default: 10 seconds
|
|
272
|
-
follow_redirects: true
|
|
271
|
+
follow_redirects: true, # Default: true
|
|
272
|
+
allow_local_network: false # Default: false
|
|
273
273
|
})
|
|
274
274
|
```
|
|
275
275
|
|
|
@@ -278,6 +278,7 @@ sheet = Cataract::Stylesheet.parse(css, import: {
|
|
|
278
278
|
- Non-CSS file extensions
|
|
279
279
|
- Circular references
|
|
280
280
|
- Excessive nesting depth
|
|
281
|
+
- SSRF - HTTP(S) fetches are validated with the [ssrf_filter](https://rubygems.org/gems/ssrf_filter) gem, which blocks loopback, private-network, link-local, and cloud metadata addresses by default (re-checked on every redirect hop). Set `allow_local_network: true` to opt out.
|
|
281
282
|
|
|
282
283
|
## Development
|
|
283
284
|
|
data/cataract.gemspec
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'uri'
|
|
4
4
|
require 'open-uri'
|
|
5
|
+
require 'ssrf_filter'
|
|
5
6
|
|
|
6
7
|
module Cataract
|
|
7
8
|
# Error raised during import resolution
|
|
@@ -38,6 +39,8 @@ module Cataract
|
|
|
38
39
|
raise ImportError, "HTTP error fetching import: #{url} (#{e.message})"
|
|
39
40
|
rescue SocketError => e
|
|
40
41
|
raise ImportError, "Network error fetching import: #{url} (#{e.message})"
|
|
42
|
+
rescue SsrfFilter::Error => e
|
|
43
|
+
raise ImportError, "Import blocked by SSRF protection: #{url} (#{e.message})"
|
|
41
44
|
rescue StandardError => e
|
|
42
45
|
raise ImportError, "Error fetching import: #{url} (#{e.class}: #{e.message})"
|
|
43
46
|
end
|
|
@@ -46,7 +49,16 @@ module Cataract
|
|
|
46
49
|
|
|
47
50
|
# Fetch content via HTTP/HTTPS
|
|
48
51
|
def fetch_http(uri, options)
|
|
49
|
-
|
|
52
|
+
if options[:allow_local_network]
|
|
53
|
+
fetch_http_unfiltered(uri, options)
|
|
54
|
+
else
|
|
55
|
+
fetch_http_ssrf_safe(uri, options)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Plain open-uri fetch, no SSRF protection - only reachable when the
|
|
60
|
+
# caller explicitly opted in via allow_local_network: true.
|
|
61
|
+
def fetch_http_unfiltered(uri, options)
|
|
50
62
|
open_uri_options = {
|
|
51
63
|
read_timeout: options[:timeout],
|
|
52
64
|
redirect: options[:follow_redirects]
|
|
@@ -55,6 +67,23 @@ module Cataract
|
|
|
55
67
|
# Use uri.open instead of URI.open to avoid shell command injection
|
|
56
68
|
uri.open(open_uri_options, &:read)
|
|
57
69
|
end
|
|
70
|
+
|
|
71
|
+
# Fetch via the ssrf_filter gem, which resolves the hostname and rejects
|
|
72
|
+
# loopback/private/link-local/metadata addresses (re-checked on every
|
|
73
|
+
# redirect hop, since a redirect can point anywhere) before connecting.
|
|
74
|
+
def fetch_http_ssrf_safe(uri, options)
|
|
75
|
+
response = SsrfFilter.get(
|
|
76
|
+
uri,
|
|
77
|
+
http_options: { read_timeout: options[:timeout], open_timeout: options[:timeout] },
|
|
78
|
+
max_redirects: options[:follow_redirects] ? SsrfFilter::DEFAULT_MAX_REDIRECTS : 0
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
82
|
+
raise OpenURI::HTTPError.new("#{response.code} #{response.message}", response)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
response.body
|
|
86
|
+
end
|
|
58
87
|
end
|
|
59
88
|
|
|
60
89
|
# Default options for safe import resolution (@import statements
|
|
@@ -68,7 +97,8 @@ module Cataract
|
|
|
68
97
|
base_path: nil, # Base path for resolving relative file imports
|
|
69
98
|
base_uri: nil, # Base URI for resolving relative HTTP imports
|
|
70
99
|
fetcher: nil, # Custom fetcher (defaults to DefaultFetcher)
|
|
71
|
-
dangerous_path_prefixes: ['/etc/', '/proc/', '/sys/', '/dev/'] # Blocked file:// path prefixes
|
|
100
|
+
dangerous_path_prefixes: ['/etc/', '/proc/', '/sys/', '/dev/'], # Blocked file:// path prefixes
|
|
101
|
+
allow_local_network: false # Block loopback/private/link-local/metadata addresses (via ssrf_filter)
|
|
72
102
|
}.freeze
|
|
73
103
|
|
|
74
104
|
# Default options for an explicitly caller-invoked load (Stylesheet#load_uri
|
|
@@ -158,7 +188,6 @@ module Cataract
|
|
|
158
188
|
|
|
159
189
|
# Additional security checks for file:// scheme
|
|
160
190
|
if uri.scheme == 'file'
|
|
161
|
-
# Resolve to absolute path to prevent directory traversal
|
|
162
191
|
file_path = uri.path
|
|
163
192
|
|
|
164
193
|
# Check file exists and is readable
|
|
@@ -166,9 +195,18 @@ module Cataract
|
|
|
166
195
|
raise ImportError, "Import file not found or not readable: #{file_path}"
|
|
167
196
|
end
|
|
168
197
|
|
|
198
|
+
# Resolve to a canonical, "..".-free path before the prefix check below -
|
|
199
|
+
# otherwise a path like '/var/../etc/hosts' doesn't literally start with
|
|
200
|
+
# '/etc/' and would slip past it, even though File.exist?/File.read above
|
|
201
|
+
# resolve it to the same file '/etc/hosts' would. Use expand_path (lexical
|
|
202
|
+
# only) rather than realpath: realpath also follows symlinks, and on
|
|
203
|
+
# macOS '/etc' itself is a symlink to '/private/etc', which would make
|
|
204
|
+
# every real path fail to start with '/etc/' and defeat this check entirely.
|
|
205
|
+
canonical_file_path = File.expand_path(file_path)
|
|
206
|
+
|
|
169
207
|
# Prevent reading sensitive files (basic check, configurable via
|
|
170
208
|
# options[:dangerous_path_prefixes] - pass [] to disable)
|
|
171
|
-
if options[:dangerous_path_prefixes]&.any? { |prefix|
|
|
209
|
+
if options[:dangerous_path_prefixes]&.any? { |prefix| canonical_file_path.start_with?(prefix) }
|
|
172
210
|
raise ImportError, "Import of sensitive system files not allowed: #{file_path}"
|
|
173
211
|
end
|
|
174
212
|
end
|
data/lib/cataract/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cataract
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Cook
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ssrf_filter
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.5'
|
|
12
26
|
description: A performant CSS parser with C extensions for accurate parsing of complex
|
|
13
27
|
CSS structures including media queries, nested selectors, and CSS Color Level 4
|
|
14
28
|
email:
|