marshalsea 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/lib/marshalsea/marshal/load_guard.rb +6 -2
- data/lib/marshalsea/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ee9dd24e014347afe27eae586c8766ed110e51dc12420219f06e7b78395137c
|
|
4
|
+
data.tar.gz: 0fc164a3530dc19efd6b0feb26d521d2f99325d566afc07b4d2b9c85a65fe0f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 276e8899a8baea7b417672b7f427df277cbb86a9a2fc0e2a57ab2f2398f79c2c0531c50596aced9d45da6655c9c218fa123d31b6fd138fc32135c99dd8bd9b31
|
|
7
|
+
data.tar.gz: 1331c714827c1a858b40e66989f27ca3081d34665df9be3d7f791c18396c4b05540df16de7d3dc1b159b0621c7caefbaaa34237df136f1fd1635fa339b0b9864
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- ©AngelaMos | 2026 -->
|
|
2
2
|
<!-- README.md -->
|
|
3
3
|
|
|
4
|
-
```
|
|
4
|
+
```ruby
|
|
5
5
|
███╗ ███╗ █████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ██╗ ███████╗███████╗ █████╗
|
|
6
6
|
████╗ ████║██╔══██╗██╔══██╗██╔════╝██║ ██║██╔══██╗██║ ██╔════╝██╔════╝██╔══██╗
|
|
7
7
|
██╔████╔██║███████║██████╔╝███████╗███████║███████║██║ ███████╗█████╗ ███████║
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
[](https://rubygems.org/gems/marshalsea)
|
|
16
16
|
[](#the-two-allowlists)
|
|
17
17
|
[](https://www.ruby-lang.org/en/news/2026/04/21/erb-cve-2026-41316/)
|
|
18
|
-
[](#build-and-test)
|
|
19
19
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
20
20
|
|
|
21
21
|
> A Ruby object-deserialization security lab. It reads `Marshal` and YAML bytes without ever reviving them, tells you which classes are in there and which methods those bytes would fire, hunts your loaded code for the classes that make usable gadgets, builds a working payload for a real 2026 CVE, and then stands up a deliberately vulnerable Sinatra target so you can watch the exploit land over HTTP and watch the defense stop it. It ships as a gem plus a container, and every defense in it comes with a written statement of what it cannot do.
|
|
@@ -30,7 +30,7 @@ It is also the bug class the industry most consistently gets wrong in retellings
|
|
|
30
30
|
|
|
31
31
|
## What it is
|
|
32
32
|
|
|
33
|
-
Not a stub. Every capability below is exercised by
|
|
33
|
+
Not a stub. Every capability below is exercised by 268 tests across seven suites and a six-stage gate that runs real containers, with 79 assertions that must all pass.
|
|
34
34
|
|
|
35
35
|
**A reader that never loads (Marshal)**
|
|
36
36
|
- Parses the Marshal binary format without calling `Marshal.load`: version bytes, type tags, instance variables, object links, symbols, floats
|
|
@@ -146,7 +146,7 @@ Every defense here is a trade, and the code says so out loud rather than in a fo
|
|
|
146
146
|
|
|
147
147
|
- **A stream that passes inspection is not a safe stream.** `proceed?` means the bytes matched a policy. It does not mean the payload is harmless, and `LIMITATION_NOTICE` says exactly that. The published CVE chain produces **zero sink tags**, so sink detection alone never catches it; only class allowlisting does.
|
|
148
148
|
- **The runtime guard is defense in depth, not a boundary.** Its cost is not a multiplier. Enabling a `TracePoint` costs a near-constant ~46 microseconds per load, so it is 1.0x on a 488 KB document and **40x on a 45-byte session cookie**, and a cookie is what this lab deserializes. It also covers the load window only: a class carrying no hook at all is built freely and fires whenever the application next touches it.
|
|
149
|
-
- **The scanner sees only what is loaded.** `ObjectSpace` cannot report a class nobody has required yet. On a stock
|
|
149
|
+
- **The scanner sees only what is loaded.** `ObjectSpace` cannot report a class nobody has required yet. On a stock `ruby:4.0-slim` it narrows 124 ungated candidates to 28 reachable, and 142 of its candidates are C-defined with no Ruby source at all, which it reports as `unanalysable` rather than scoring as inert. Those figures are a statement about what one process had loaded, not about Ruby; re-run `just scan` rather than quoting them.
|
|
150
150
|
- **The gem floor is `>= 3.4` and it was measured, not chosen.** `Marshal.load` did not validate the bignum sign byte until 3.4. The parser accepts `+` and `-` only, so it models 3.4 and newer; run it on 3.3 and it disagrees with the interpreter it exists to model. `just package` re-proves that in both directions on every run.
|
|
151
151
|
|
|
152
152
|
## Architecture
|
|
@@ -19,6 +19,10 @@ module Marshalsea
|
|
|
19
19
|
REASON = "deserialization hook %s#%s is not permitted"
|
|
20
20
|
ANONYMOUS_OWNER = "(class with no name)"
|
|
21
21
|
|
|
22
|
+
CLASS_OF = ::Object.instance_method(:class).freeze
|
|
23
|
+
KIND_OF = ::Object.instance_method(:is_a?).freeze
|
|
24
|
+
NAME_OF = ::Module.instance_method(:name).freeze
|
|
25
|
+
|
|
22
26
|
LIMITATION_NOTICE = <<~NOTICE
|
|
23
27
|
SECURITY LIMITATION
|
|
24
28
|
|
|
@@ -109,8 +113,8 @@ module Marshalsea
|
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
def owner_name(receiver)
|
|
112
|
-
owner =
|
|
113
|
-
name = owner
|
|
116
|
+
owner = KIND_OF.bind_call(receiver, ::Module) ? receiver : CLASS_OF.bind_call(receiver)
|
|
117
|
+
name = NAME_OF.bind_call(owner)
|
|
114
118
|
name if name.is_a?(String) && !name.empty?
|
|
115
119
|
rescue StandardError
|
|
116
120
|
nil
|
data/lib/marshalsea/version.rb
CHANGED