Module: Cataract::ImportResolver
- Defined in:
- lib/cataract/import_resolver.rb
Overview
Resolves @import statements in CSS Handles fetching imported files and inlining them with proper security controls
Defined Under Namespace
Classes: DefaultFetcher
Constant Summary collapse
- SAFE_DEFAULTS =
Default options for safe import resolution (@import statements discovered while parsing a stylesheet - untrusted by default)
{ max_depth: 5, # Prevent infinite recursion allowed_schemes: ['https'], # Only HTTPS by default extensions: ['css'], # Only .css files timeout: 10, # 10 second timeout for fetches follow_redirects: true, # Follow redirects base_path: nil, # Base path for resolving relative file imports base_uri: nil, # Base URI for resolving relative HTTP imports fetcher: nil, # Custom fetcher (defaults to DefaultFetcher) dangerous_path_prefixes: ['/etc/', '/proc/', '/sys/', '/dev/'] # Blocked file:// path prefixes }.freeze
- LOAD_DEFAULTS =
Default options for an explicitly caller-invoked load (Stylesheet#load_uri / #load_file). The caller chose this exact URI/path themselves - a different trust model than @import content discovered inside a stylesheet - so this permits the schemes/extensions load_uri has always supported, while still keeping the dangerous_path_prefixes protection from SAFE_DEFAULTS as a default (callers can still override it).
SAFE_DEFAULTS.merge( allowed_schemes: %w[http https file], extensions: :any # skip extension validation entirely - see validate_url ).freeze
Class Method Summary collapse
-
.normalize_options(options, defaults: SAFE_DEFAULTS) ⇒ Object
Normalize options with safe defaults.
-
.normalize_url(url, base_path: nil, base_uri: nil) ⇒ Object
Normalize URL - handle relative paths and missing schemes Returns a URI object.
-
.validate_url(url, options) ⇒ Object
Validate URL against security options.
Class Method Details
.normalize_options(options, defaults: SAFE_DEFAULTS) ⇒ Object
Normalize options with safe defaults
90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/cataract/import_resolver.rb', line 90 def self.(, defaults: SAFE_DEFAULTS) if == true # imports: true -> use defaults as-is defaults.dup elsif .is_a?(Hash) # imports: { ... } -> merge with defaults defaults.merge() else raise ArgumentError, 'imports option must be true or a Hash' end end |
.normalize_url(url, base_path: nil, base_uri: nil) ⇒ Object
Normalize URL - handle relative paths and missing schemes Returns a URI object
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/cataract/import_resolver.rb', line 108 def self.normalize_url(url, base_path: nil, base_uri: nil) # Try to parse as-is first uri = URI.parse(url) # If no scheme, treat as relative path if uri.scheme.nil? # If we have a base_uri (HTTP/HTTPS), resolve against it if base_uri base = URI.parse(base_uri) uri = base.merge(url) elsif url.start_with?('/') # Absolute file path uri = URI.parse("file://#{url}") else # Relative file path - make it absolute relative to base_path or current directory absolute_path = if base_path File.(url, base_path) else File.(url) end uri = URI.parse("file://#{absolute_path}") end end uri rescue URI::InvalidURIError => e raise ImportError, "Invalid import URL: #{url} (#{e.})" end |
.validate_url(url, options) ⇒ Object
Validate URL against security options
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/cataract/import_resolver.rb', line 138 def self.validate_url(url, ) uri = normalize_url(url, base_path: [:base_path], base_uri: [:base_uri]) # Check scheme unless [:allowed_schemes].include?(uri.scheme) raise ImportError, "Import scheme '#{uri.scheme}' not allowed. Allowed schemes: #{[:allowed_schemes].join(', ')}" end # Check extension (extensions: :any skips this check entirely - used by # LOAD_DEFAULTS, since a directly-invoked load isn't restricted to .css) unless [:extensions] == :any path = uri.path || '' ext = File.extname(path).delete_prefix('.') unless ext.empty? || [:extensions].include?(ext) raise ImportError, "Import extension '.#{ext}' not allowed. Allowed extensions: #{[:extensions].join(', ')}" end end # Additional security checks for file:// scheme if uri.scheme == 'file' # Resolve to absolute path to prevent directory traversal file_path = uri.path # Check file exists and is readable unless File.exist?(file_path) && File.readable?(file_path) raise ImportError, "Import file not found or not readable: #{file_path}" end # Prevent reading sensitive files (basic check, configurable via # options[:dangerous_path_prefixes] - pass [] to disable) if [:dangerous_path_prefixes]&.any? { |prefix| file_path.start_with?(prefix) } raise ImportError, "Import of sensitive system files not allowed: #{file_path}" end end true rescue URI::InvalidURIError => e raise ImportError, "Invalid import URL: #{url} (#{e.})" end |