Class: Scryer::Rules::PathTraversalRule
- Inherits:
-
Scryer::Rule
- Object
- Scryer::Rule
- Scryer::Rules::PathTraversalRule
- Defined in:
- lib/scryer/rules/path_traversal_rule.rb
Overview
Flags a filesystem operation (File.join/read/open/new/write/delete,
Dir.glob/entries, send_file) where an argument references params
directly — without sanitization, ../../etc/passwd-style path
segments in the request let an attacker read (or write/delete) files
outside whatever directory the code intended to restrict access to.
Constant Summary collapse
- DANGEROUS_CALLS =
{ "File" => %w[join read open new write delete binread binwrite], "Dir" => %w[glob entries] }.freeze
- BARE_METHODS =
%w[send_file send_data].freeze
Instance Attribute Summary
Attributes inherited from Scryer::Rule
Instance Method Summary collapse
Methods inherited from Scryer::Rule
Constructor Details
This class inherits a constructor from Scryer::Rule
Instance Method Details
#scan ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/scryer/rules/path_traversal_rule.rb', line 23 def scan findings = [] Ast.each_node(sexp) do |node| next unless Ast.tagged?(node, :method_add_arg, :command, :command_call) inner = Ast.tagged?(node, :method_add_arg) ? node[1] : node receiver_and_name = Ast.call_name(inner) next unless receiver_and_name receiver, method_name = receiver_and_name next unless dangerous_call?(receiver, method_name) args = Ast.call_arguments(node) # `send_file`/`send_data`'s keyword options (`filename:`, `type:`, # `disposition:`) control response metadata (the Content-Disposition # filename the browser shows, the content-type header, inline vs. # attachment) — none of them ever touch the filesystem. Only the # first *positional* argument (the actual path for send_file, the # raw bytes for send_data) is ever used as a path, so a `params` # reference confined to the keyword-args hash isn't a path # traversal risk and would otherwise be a false positive on the # extremely common `send_file path, filename: params[:filename]` # idiom. This exclusion is scoped to BARE_METHODS specifically — # File.join/Dir.glob/etc. never take a keyword-args hash in # practice, so they're unaffected. path_args = if receiver.nil? && BARE_METHODS.include?(method_name) args.reject { |a| Ast.tagged?(a, :bare_assoc_hash) } else args end next unless path_args.any? { |a| Ast.references_params?(a) && !sanitized_via_basename?(a) } line = Ast.line_of(node) findings << finding( line: line, message: "`#{describe_call(receiver, method_name)}` is called with a path/argument " \ "that references `params` — a value like `../../config/master.key` reaches " \ "the filesystem unchanged, letting an attacker read or write files outside " \ "whatever directory this was meant to be scoped to.", suggested_fix: "Reduce the input to a safe basename before using it " \ "(`File.basename(params[:name])`), and/or verify the resolved path " \ "stays inside the intended directory (compare " \ "`File.expand_path(...)` against the allowed root) before touching " \ "the filesystem." ) end findings end |