Class: Scryer::Rules::UnsafeDeserializationRule

Inherits:
Scryer::Rule
  • Object
show all
Defined in:
lib/scryer/rules/unsafe_deserialization_rule.rb

Overview

Flags Marshal.load, YAML.load (as opposed to YAML.safe_load), and JSON.load (as opposed to JSON.parse) — all three can instantiate arbitrary Ruby objects from untrusted input, a known RCE vector in Rails apps (several real-world CVEs trace back to exactly this).

Constant Summary collapse

UNSAFE_CALLS =
{
  %w[Marshal load] => "Marshal.load can instantiate arbitrary Ruby objects, including ones " \
                        "that execute code as a side effect of being constructed — never call " \
                        "it on data that came from a user, request, or external service.",
  %w[YAML load] => "YAML.load (unlike YAML.safe_load) can instantiate arbitrary Ruby objects " \
                     "from the document, which is a known remote-code-execution vector when " \
                     "the YAML source isn't fully trusted.",
  %w[JSON load] => "JSON.load can invoke arbitrary `create_id`-tagged object construction, " \
                     "unlike the safer JSON.parse."
}.freeze

Instance Attribute Summary

Attributes inherited from Scryer::Rule

#file, #sexp, #source

Instance Method Summary collapse

Methods inherited from Scryer::Rule

inherited, #initialize

Constructor Details

This class inherits a constructor from Scryer::Rule

Instance Method Details

#scanObject



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
# File 'lib/scryer/rules/unsafe_deserialization_rule.rb', line 24

def scan
  findings = []

  Ast.each_node(sexp) do |node|
    # Only match the method_add_arg wrapper (call + its parenthesized
    # args) — the :call node it wraps would otherwise also match this
    # loop on its own and double-count every finding.
    next unless Ast.tagged?(node, :method_add_arg)

    inner = node[1]
    next unless Ast.tagged?(inner, :call)

    receiver = inner[1]
    method_name = Ast.ident_text(inner[3])
    receiver_name = Ast.ident_text(receiver.is_a?(Array) ? receiver[1] : nil) if Ast.tagged?(receiver, :var_ref)

    match = UNSAFE_CALLS.keys.find { |(recv, meth)| recv == receiver_name && meth == method_name }
    next unless match

    line = Ast.line_of(node)
    findings << finding(
      line: line,
      message: UNSAFE_CALLS[match],
      suggested_fix: safe_alternative(match)
    )
  end

  findings
end