Cybersecurity-Projects/PROJECTS/beginner/deserialization-gadget-lab/test/marshal/parser_test.rb

228 lines
6.5 KiB
Ruby

# ©AngelaMos | 2026
# parser_test.rb
require_relative "../test_helper"
module Rube
module Marshal
class ParserTest < Minitest::Test
def parse(blob)
Parser.new(blob).parse
end
def roundtrip(object)
parse(::Marshal.dump(object)).root
end
def test_rejects_empty_stream
assert_raises(TruncatedStreamError) { parse("") }
end
def test_rejects_header_only
assert_raises(TruncatedStreamError) { parse("\x04\x08") }
end
def test_rejects_wrong_major_version
assert_raises(UnsupportedVersionError) { parse("\x05\x08\x30") }
end
def test_rejects_future_minor_version
assert_raises(UnsupportedVersionError) { parse("\x04\x09\x30") }
end
def test_accepts_older_minor_version
assert_equal :nil, parse("\x04\x07\x30").root.type
end
def test_rejects_unknown_tag
assert_raises(UnknownTagError) { parse("\x04\x08\x00") }
end
def test_rejects_trailing_bytes
assert_raises(TrailingBytesError) { parse(::Marshal.dump(nil) + "junk") }
end
def test_parses_nil
assert_equal :nil, roundtrip(nil).type
end
def test_parses_booleans
assert_equal true, roundtrip(true).value
assert_equal false, roundtrip(false).value
end
def test_parses_fixnum_across_every_encoding_width
[0, 1, 5, 122, 123, 255, 256, 65_535, 65_536, 1_073_741_823,
-1, -123, -124, -256, -65_536, -1_073_741_824].each do |n|
assert_equal n, roundtrip(n).value, "fixnum #{n} did not round-trip"
end
end
def test_parses_bignum_both_signs
[2**64, -(2**64), 2**128 + 7].each do |n|
assert_equal n, roundtrip(n).value, "bignum #{n} did not round-trip"
end
end
def test_parses_float
assert_in_delta 3.5, roundtrip(3.5).value, 0.0
end
def test_parses_symbol
assert_equal :marshal_load, roundtrip(:marshal_load).value
end
def test_parses_string_through_ivar_wrapper
node = roundtrip("hello")
assert_equal :string, node.type
assert_equal "hello", node.value
end
def test_parses_array
node = roundtrip([1, 2, 3])
assert_equal :array, node.type
assert_equal [1, 2, 3], node.children.map(&:value)
end
def test_parses_nested_array
node = roundtrip([1, [2, [3]]])
assert_equal 3, node.children.last.children.last.children.first.value
end
def test_parses_hash
node = roundtrip({ a: 1 })
assert_equal :hash, node.type
assert_equal 1, node.children.length
end
def test_object_links_are_zero_indexed
cyclic = []
cyclic << cyclic
node = roundtrip(cyclic)
link = node.children.first
assert_equal :object_link, link.type
assert_equal 0, link.value
end
def test_rejects_out_of_bounds_object_link
assert_raises(InvalidLinkError) { parse("\x04\x08@\x0a") }
end
def test_rejects_out_of_bounds_symlink
assert_raises(InvalidLinkError) { parse("\x04\x08;\x0a") }
end
def test_resolves_symlink_to_prior_symbol
node = roundtrip([:same, :same])
assert_equal :symlink, node.children.last.type
assert_equal :same, node.children.last.value
end
def test_extracts_class_name_from_plain_object
blob = ::Marshal.dump(Fixture.new)
result = parse(blob)
assert_includes result.class_names, "Rube::Marshal::ParserTest::Fixture"
end
def test_extracts_class_name_without_instantiating
refute Fixture.instantiated, "parser must not construct the class"
parse(::Marshal.dump(Fixture.new))
refute Fixture.instantiated, "parser instantiated a class during parse"
end
def test_flags_usermarshal_as_sink
result = parse(::Marshal.dump(UserMarshalFixture.new))
sink = result.sinks.first
refute_nil sink
assert_equal "marshal_load", sink.sink_method
assert_equal "Rube::Marshal::ParserTest::UserMarshalFixture", sink.class_name
end
def test_flags_userdef_as_sink
result = parse(::Marshal.dump(UserDefFixture.new))
assert_equal "_load", result.sinks.first.sink_method
end
def test_clean_payload_reports_no_sinks
assert_empty parse(::Marshal.dump([1, "two", :three, { a: 1 }])).sinks
end
def test_rejects_truncated_string_body
assert_raises(TruncatedStreamError) { parse("\x04\x08\"\x0aab") }
end
def test_rejects_truncated_array_elements
assert_raises(TruncatedStreamError) { parse("\x04\x08[\x08i\x06") }
end
def test_rejects_truncated_multibyte_fixnum
assert_raises(TruncatedStreamError) { parse("\x04\x08i\x03\x01") }
end
def test_rejects_depth_beyond_limit
deep = "\x04\x08" + ("[\x06" * (Constants::DEFAULT_MAX_DEPTH + 5)) + "0"
assert_raises(DepthLimitError) { parse(deep) }
end
def test_honours_custom_depth_limit
blob = ::Marshal.dump([[[1]]])
assert_raises(DepthLimitError) { Parser.new(blob, max_depth: 2).parse }
end
def load_watcher
fired = false
tracer = TracePoint.new(:call, :c_call) do |tp|
fired = true if tp.method_id == :load && tp.self.equal?(::Marshal)
end
tracer.enable { yield }
fired
end
def test_load_watcher_oracle_is_live
assert load_watcher { ::Marshal.load(::Marshal.dump([1, 2])) },
"oracle failed to observe a real Marshal.load, so the next test would pass vacuously"
end
def test_never_calls_marshal_load
blob = ::Marshal.dump(UserMarshalFixture.new)
refute load_watcher { parse(blob) }, "parser invoked Marshal.load"
end
def test_never_calls_marshal_load_on_gadget_shaped_payload
blob = ::Marshal.dump(Gem::Requirement.new(">= 0"))
refute load_watcher { parse(blob) }, "parser invoked Marshal.load"
end
class Fixture
@instantiated = false
class << self
attr_accessor :instantiated
end
def initialize
@marker = "value"
end
end
class UserMarshalFixture
def marshal_dump
["payload"]
end
def marshal_load(data); end
end
class UserDefFixture
def _dump(_depth)
"opaque"
end
def self._load(_data)
new
end
end
end
end
end