# Unsafe-deserialization / XXE pre-scan (ARION service)

Deterministic pre-scan for the deserialization and XML-parser classes
that are checkable offline, line-wise, without executing code — the
sinks behind the classic RCE/object-injection and XXE bug families
across Python, JVM, .NET, PHP, Ruby, Node, and Go.

No inference, no network — every check is a property of the source
line (plus one file-level hardening check for XML parsers). Useful
before merging a dependency, onboarding a repo, or scoping where a
manual audit should spend its time.

**Honest scope:** this is a *pre-scan*, not an audit. Line-based
matching cannot prove the sink is reachable from attacker input, see
runtime parser configuration, or resolve framework defaults. A FLAG
means "this line deserves human review"; a clean report is not proof
deserialization is safe.

## Findings

| code | sev | signature |
|---|---|---|
| `PICKLE-LOAD` | HIGH | `pickle/cPickle/dill.load(s)`, `pickle.Unpickler` — arbitrary code execution on untrusted input |
| `YAML-UNSAFE-LOAD` | HIGH/MED | `yaml.load` without a safe loader (HIGH in Python — unsafe by default; MEDIUM for js-yaml v4+ default-safe schema), `unsafe_load` HIGH, `full_load` MEDIUM; `Loader=SafeLoader`/`safe_load` suppressed |
| `MARSHAL-LOAD` | MEDIUM | `marshal.load(s)` — code-adjacent object format |
| `JSONPICKLE-DECODE` | MEDIUM | `jsonpickle.decode/loads` — arbitrary class decode |
| `SHELVE-OPEN` | LOW | `shelve.open` — pickle-backed store |
| `NUMPY-PICKLE` | HIGH | `np.load(..., allow_pickle=True)` |
| `PANDAS-READPICKLE` | MEDIUM | `pd.read_pickle` |
| `JAVA-DESERIALIZE` | HIGH | `new ObjectInputStream`, `readObject`, `readUnshared`, `XMLDecoder` |
| `JACKSON-DEFAULT-TYPING` | HIGH | `enableDefaultTyping`/`activateDefaultTyping`/`DefaultTyping.`/`@JsonTypeInfo(Id.CLASS)` |
| `XSTREAM` | MEDIUM | `new XStream`, `.fromXML` — needs explicit allowlist |
| `SNAKEYAML-LOAD` | MEDIUM | `new Yaml(`, snakeyaml imports, raw `new Constructor(` — default allows arbitrary classes |
| `KRYO-DESERIALIZE` | MEDIUM | `readClassAndObject`, `setRegistrationRequired(false)` |
| `DOTNET-DESERIALIZE` | HIGH | `BinaryFormatter`, `SoapFormatter`, `NetDataContractSerializer`, `LosFormatter`, `ObjectStateFormatter`, `JavaScriptSerializer` |
| `DOTNET-TYPENAMEHANDLING` | HIGH | Newtonsoft `TypeNameHandling = All/Auto/Objects/Arrays` (None suppressed) |
| `PHP-UNSERIALIZE` | HIGH/MED | `unserialize()` HIGH, `maybe_unserialize()` MEDIUM |
| `RUBY-MARSHAL` | HIGH | `Marshal.load`/`restore` |
| `RUBY-YAML-LOAD` | MEDIUM | `YAML.load`/`Psych.load` (Psych < 4 unsafe) |
| `NODE-DESERIALIZE-EVAL` | HIGH | `node-serialize`, `serialize-to-js`, `funcster`, `serialize.js` — eval-bearing unserialize |
| `JS-YAML-LOAD` | MEDIUM | `yaml.load`/`jsyaml.load` in JS/TS files |
| `GO-GOB-DECODE` | LOW | `gob.NewDecoder` — panics/type-confusion on untrusted input, not RCE-class |
| `XXE-ENTITIES-ON` | HIGH | entity expansion explicitly enabled: `resolve_entities=True`, `LIBXML_NOENT`, `setExpandEntityReferences(true)`, `SUPPORT_DTD true`, `IS_SUPPORTING_EXTERNAL_ENTITIES true` |
| `XXE-JAVA-PARSER` | MEDIUM | `DocumentBuilderFactory`/`SAXParserFactory`/`XMLInputFactory`/`XMLReaderFactory`/`SAXReader`/`DocumentHelper`/`TransformerFactory`/`XPathFactory`/`SchemaFactory` without visible hardening |
| `XXE-PY-PARSER` | MEDIUM | `xml.etree`/`minidom`/`pulldom`/`sax`/`lxml.etree`/`ET.*` parse calls without hardening |
| `XXE-DOTNET` | MEDIUM | `XmlDocument`/`XmlReader.Create`/`XslCompiledTransform`/`XmlTextReader` without hardening |
| `XXE-PHP` | MEDIUM | `simplexml_load_*`/`->loadXML`/`->loadHTML`/`DOMDocument`/`xml_parse` without `LIBXML_NONET` |

## Context rules

- **XML hardening markers anywhere in the same file** — `disallow-doctype`,
  `FEATURE_SECURE_PROCESSING`, `ACCESS_EXTERNAL_DTD/SCHEMA`,
  `SUPPORT_DTD … false`, `IS_SUPPORTING_EXTERNAL_ENTITIES … false`,
  `setExpandEntityReferences(false)`, `defusedxml`,
  `resolve_entities=False`, `no_network=True`, `XmlResolver = null`,
  `DtdProcessing.Prohibit/Ignore`, `LIBXML_NONET` — downgrade
  XXE parser hits MEDIUM → LOW (annotated "verify it covers this
  instance"). `XXE-ENTITIES-ON` is never downgraded.
- **`yaml.load` with `Loader=SafeLoader`/`safe_load`** → suppressed;
  explicit unsafe `Loader=`/`FullLoader` → MEDIUM (JS/TS) or HIGH.
- **`maybe_unserialize`** → MEDIUM (WP-style stored-object path).
- **JVM `yaml.load`** → handled by `SNAKEYAML-LOAD`, not double-flagged.
- **Same-line `XXE-ENTITIES-ON`** → the redundant `XXE-PY-PARSER`
  shadow finding is dropped.

## Usage

```sh
python3 deser_prescan.py <file-or-dir>          # markdown report
python3 deser_prescan.py <file-or-dir> --json   # machine-readable
python3 deser_prescan.py --selftest             # 31-vector selftest
```

Stdlib only. Exit 0 = no HIGH/MEDIUM findings, 1 = findings,
2 = usage error — safe as a CI gate.

## Validation

- 31/31 selftest vectors pass (`--selftest`), covering every finding
  class incl. safe-loader suppression, TypeNameHandling.None
  suppression, defusedxml/disallow-doctype downgrade, and
  entity-expansion shadow dedup.
- Live-validated: clean 253-file service repo → zero findings;
  Python stdlib `xml/` tree → 3 true-positive parser call sites;
  crafted hostile sample → all 8 HIGH + 5 MEDIUM classes caught
  (see `sample-report.md`).

## Pricing

300 sats / ~$0.30 per repo scan, delivered as a markdown report like
`sample-report.md`. Order via the ARION service channels on the
operator dashboard — specify the repo (tarball URL, git URL, or file
drop) and any scope notes.
