Class: Marshalsea::Psych::Inspector

Inherits:
Object
  • Object
show all
Defined in:
lib/marshalsea/psych/inspector.rb

Constant Summary collapse

Decision =
Marshalsea::Marshal::BoundaryDetector::Decision
REASON_INPUT_TYPE =
"input is not a String"
REASON_MALFORMED =
"document is not parseable YAML: %s"
REASON_UNAPPROVED =
"document revives unapproved class %s through %s"
REASON_KEY_DISPATCH =
"document puts %s in a mapping key, so its #hash and #== run " \
"while the mapping is rebuilt"
LIMITATION_NOTICE =
<<~NOTICE
  SECURITY LIMITATION

  Marshalsea::Psych::Inspector reads a YAML document through Psych.parse_stream, which
  builds an AST and revives nothing. It never calls YAML.load or YAML.unsafe_load.

  Unlike Marshal, Psych's own allowlist is a real veto: Psych checks the tag before it
  revives the object, where Marshal runs its proc after the callback has already fired.
  Same intent, opposite outcome, decided entirely by where the check sits. That means
  YAML.safe_load with permitted_classes is a boundary and this inspector is only
  detection and reporting on top of it.

  Prefer YAML.safe_load. Use this to see what a document would revive, to log it, or
  to reject a document before it reaches a loader you do not control.

  Alias expansion is not performed here, so an alias bomb costs nothing to inspect.
  It still costs whatever the eventual loader spends expanding it, which is why the
  alias count is bounded and reported rather than ignored.
NOTICE

Instance Method Summary collapse

Constructor Details

#initialize(permitted_class_names: [], limits: Limits.new) ⇒ Inspector

Returns a new instance of Inspector.



158
159
160
161
# File 'lib/marshalsea/psych/inspector.rb', line 158

def initialize(permitted_class_names: [], limits: Limits.new)
  @permitted_class_names = permitted_class_names.map(&:to_s).freeze
  @limits = limits
end

Instance Method Details

#inspect_document(input) ⇒ Object



163
164
165
166
167
168
169
170
171
# File 'lib/marshalsea/psych/inspector.rb', line 163

def inspect_document(input)
  return reject(REASON_INPUT_TYPE) unless input.is_a?(String)

  snapshot = input.dup.freeze
  enforce_size(snapshot)
  evaluate(read(snapshot), snapshot)
rescue DocumentError => e
  reject(format(REASON_MALFORMED, e.class.name.split("::").last))
end

#read(source) ⇒ Object



173
174
175
176
177
# File 'lib/marshalsea/psych/inspector.rb', line 173

def read(source)
  Walk.new(limits).call(::Psych.parse_stream(source))
rescue ::Psych::SyntaxError => e
  raise MalformedDocumentError, e.message
end