Parser Differentials: When Interpretation Becomes a Vulnerability

Joernchen (Joern Schneeweisz) (Principal Security Engineer · GitLab)

OffensiveCon 2025 · Day 1 · Main · Briefings

Overview

Joern Schneeweisz (Joernchen) of GitLab's Security Research team walks through a decade of accumulated parser differential exploits — from a 2018 CouchDB RCE to a 2024 GitLab Workspaces arbitrary file write — arguing that format disagreements between parsers are stockpilable assets, not just one-off bugs. The practical crown jewel is a three-language YAML file that simultaneously presents different key values to Go, Python, and Ruby, enabling security control bypass and, when combined with a path traversal primitive, real shell access. ---

Watch on YouTube

Visual summary for Parser Differentials: When Interpretation Becomes a Vulnerability by Joernchen (Joern Schneeweisz)
Visual summary for Parser Differentials: When Interpretation Becomes a Vulnerability by Joernchen (Joern Schneeweisz)

Key moments

  1. 3:38 Stockpiling methodology: accumulate differentials before finding targets
  2. 4:16 CouchDB RCE: Erlang/JS JSON duplicate key splits admin role validation
  3. 7:35 Kubernetes JWT: HTTP header iteration direction exploited for auth bypass
  4. 13:39 YAML binary tag mechanics: global/local tags split Go, Python, Ruby parsing
  5. 17:47 Malformed Base64 padding as Go/Ruby splitter — three-way polyglot complete
  6. 22:03 GitLab devfile CVE: Ruby/Go YAML split bypasses parent-key security filter
  7. 26:32 Ruby SAML: dual Nokogiri/REXML parsers allow freely-forged signed XML document

Parser Differentials: When Interpretation Becomes a Vulnerability

Speaker: Joernchen / Joern Schneeweisz (GitLab Security Research)

Conference: OffensiveCon 2025 — May 16–17, 2025, Berlin

YouTube: https://www.youtube.com/watch?v=Dq_KVLXzxH8

Reading time: ~9 minutes

TL;DR

Joern Schneeweisz (Joernchen) of GitLab's Security Research team walks through a decade of accumulated parser differential exploits — from a 2018 CouchDB RCE to a 2024 GitLab Workspaces arbitrary file write — arguing that format disagreements between parsers are stockpilable assets, not just one-off bugs. The practical crown jewel is a three-language YAML file that simultaneously presents different key values to Go, Python, and Ruby, enabling security control bypass and, when combined with a path traversal primitive, real shell access.

Introduction

A parser differential exists wherever two or more parsers, each consuming the same input, produce different interpretations of it. The concept is formally simple but operationally slippery: the security impact of any given differential is entirely context-dependent. A discrepancy in how two JSON parsers handle duplicate keys means nothing in isolation — but placed across a front-end authentication check and a back-end authorization decision, it becomes an authentication bypass with trivial proof-of-concept payloads.

Joernchen has been accumulating parser differentials since encountering a particularly elegant CouchDB RCE around 2018. At OffensiveCon 2025, he presented his personal journey through this bug class: not a comprehensive survey, not a fuzzing methodology, but a narrative of discovered and exploited differentials, culminating in a Parser Differential Award-winning YAML chain that enabled an arbitrary file write in GitLab's Workspaces feature. His framing — "stockpile parser differentials, then find a use case" — challenges the conventional approach of finding a target and then hunting bugs; instead, it treats differentials as pre-built offensive primitives awaiting the right deployment context.

The CouchDB Blueprint: Duplicate Keys Across Erlang and JavaScript

▶ Watch: CouchDB RCE — duplicate JSON key parser differential (04:16)

The foundational example is a CouchDB remote code execution discovered by Max Justice circa 2017–2018. CouchDB is implemented in Erlang but supports JavaScript validation functions for document storage. When a JSON object contains a duplicate key, the two languages diverge: Erlang's jiffy library returns an array of all values seen under that key and the getter returns the first, while JavaScript's JSON parser discards all but the last value.

The exploit payload is minimal: create a user document with the roles key duplicated, containing ["_admin"] as the first value and [] as the second. Erlang sees an admin user (acting on the first value), while the JavaScript validation function — which checks whether the user is attempting to claim admin rights — sees an empty roles array and approves the operation. An unauthenticated attacker can thus create an arbitrary admin account, leading to RCE through CouchDB's design.

Joernchen uses this example as a teaching template because it illustrates the differential's essential property: the payload is deterministic and requires no heap grooming, no timing, no environment-specific conditions. If the parser combination is vulnerable, the payload works every time.

JWT Parsing Order in Kubernetes Ingress: Direction Matters

▶ Watch: Kubernetes ingress JWT duplicate header exploit (07:35)

The second case comes from Joernchen's consulting tenure at Precurity Labs, involving a Kubernetes cluster where JWT verification was split between an ingress controller and application backends. The ingress controller verified the signature and expiry of the JWT in the Authorization header; backends decoded but did not re-verify it. When Joernchen pointed out this asymmetry, the customer responded that since the signature was already verified at the ingress, the back-end's unverified decoding was acceptable.

The parser differential lay in how the two Java libraries iterated over HTTP headers. The ingress controller used getFirst() on the headers object — which, counterintuitively, returned the last Authorization header when multiple were present. The backend iterated headers in forward order and stopped at the first match. This direction reversal meant: send two Authorization headers, first an unsigned "algorithm none" payload claiming admin status, then a legitimately signed token for a regular user. The ingress controller validated the signed token (last in the list), the backend decoded the unsigned admin token (first in the list).

The exploit is a two-liner: the first Authorization header contains an unsigned JWT claiming admin privileges; the second contains a valid signed JWT for a low-privilege user. The mitigation principle Joernchen derives from this: instead of passing through what you saw, re-emit what you understood — strip the raw header and emit only the normalized form of the credential your component actually validated.

YAML Polyglot: Three Languages, One File

▶ Watch: YAML parser differential — Python, Ruby, and Go (12:12)

In 2023, Joernchen found a YAML parser differential between Python and Ruby: the same YAML key could return different values under each language's standard library. He published it without a specific use case. A year later, he added Go to the mix and constructed a YAML file that simultaneously presents three different values for the same key to Go, Python, and Ruby — winning the LangSec Parser Differential Award.

▶ Watch: Binary tag mechanics — global vs. local YAML tags (13:38)

The construction exploits three orthogonal YAML parser behaviors:

  1. Duplicate keys. YAML formally prohibits duplicate keys, but implementations diverge: Ruby silently takes the last value, Python's SafeLoader also takes the last, and Go's parser rejects the document entirely.
  1. Global tags (!!binary). The !!binary global tag instructs the parser to Base64-decode the tagged value. In keys, this means the parser must Base64-decode the key before testing for uniqueness. Go does this correctly; Python's base loader does not recognize the global tag and treats the encoded key as a literal string. This splits Go/Ruby behavior from Python.
  1. Local tags (!binary). A single ! prefix defines a local tag. Ruby assumes local !binary also implies Base64 decoding; Go takes the tagged key literally as a string. A second differential: Go's key uniqueness check strips tags before comparing, so !!binary and !binary with the same underlying content are treated as the same key — causing a duplicate key error rather than the desired split.
  1. Malformed Base64. Ruby's YAML parser accepts Base64 strings without trailing padding (= signs); Go's rejects them as invalid. This provides a clean Go/Ruby splitter: use an unpadded Base64 string where Go will error and Ruby will decode successfully.

Combining these four behaviors, a single YAML document can deliver value A to Python, value B to Go, and value C to Ruby — a three-way polyglot. This was demonstrated to bypass all three tested IaC security scanners, since the scanners parsed the YAML differently from the runtime that would actually execute the configuration.

GitLab Workspaces: Parser Differential Enables Path Traversal

▶ Watch: GitLab devfile vulnerability — Ruby/Go YAML split enabling file write (21:03)

The stockpile paid off with a real vulnerability in GitLab's Workspaces feature (CVE disclosed in 2024). Workspaces allow users to run remote VS Code environments on Kubernetes pods, defined by devfiles — YAML configuration documents. GitLab's implementation had one explicitly prohibited feature: the parent key, which would allow a devfile to inherit from a remote base (a Git repo or devfile registry). The parent-fetching mechanism contained an arbitrary file write primitive via a path traversal in .tar.gz extraction.

▶ Watch: Path traversal achieved via YAML differential bypass (22:19)

The enforcement architecture was split: Ruby parsed the user-submitted devfile and rejected any document containing the parent key, then passed the validated YAML to an external Go binary (the devfile gem) for actual processing. By applying the Ruby/Go YAML differential, Joernchen constructed a devfile where Ruby saw no parent key and approved the document, while Go's parser decoded the YAML and saw the parent key fully intact — triggering the file write primitive downstream.

In this instance, the parser differential is not itself the vulnerability; it is the enabler that bypasses a security control to expose an existing vulnerability. Joernchen draws an analogy to SSRF: the differential gets you access to an endpoint, but the endpoint must have something to exploit. The combination of a differential (language disagreement as bypass) and a primitive (tar.gz traversal as impact) constitutes the full attack chain.

Wild-Caught Differentials and Broader Implications

▶ Watch: Six-language YAML polyglot and broader ecosystem examples (24:45)

After publishing the three-language YAML polyglot, a researcher contacted Joernchen with an extension to six languages: Ruby, Rust, Node.js, Go, Java, and Python — all disagreeing on the same YAML document. The write-up is publicly available on GitHub.

Joernchen surveys broader historical examples:

  • Android MasterKey (2013): APK signatures were bypassed because the zip file was parsed differently in C (native layer) versus Java (Dalvik VM). The C parser would see the malicious entry; the Java verifier would see the signed one.
  • Psychic Paper (iOS): XML comments parsed differently in multiple iOS components enabled a sandbox escape.
  • Ruby SAML (2024): Two XML parsers within a single library (Nokogiri and REXML) handled the same signed XML document differently. One parser treated the entire document as signed; the other only validated a subtree — allowing the attacker to freely modify the unsigned portion while the signature remained valid.

The talk closes with a strategic observation: as data formats grow more complex, the specification surface for parser disagreements grows proportionally. HTTP alone has decades of parser differential exploits (WAF bypasses, request smuggling chains), documented by researchers like James Kettle and Orange Tsai. Joernchen's advice: build your stockpile proactively, then deploy when target context provides the right combination.

Notable Quotes

"You have two parsers, and they disagree, and we're trying to maximize the profit we can make with this."

"You don't pass on what you saw, but pass on to the next parser what you understood."

"We want a shell, right?"

"For a parser differential, you can just take two parsers for the same format and try to compare them — kind of stockpile on parser differentials without any further meaning. But then when you need them, you have them."

Key Takeaways

  • Parser differentials are stockpilable. A differential with no immediate use case today may be the exact primitive needed for a target discovered later. Treat them as inventory.
  • Impact is entirely context-dependent. A duplicate key disagreement is inert unless it crosses a trust boundary — front-end auth versus back-end processing, scanner versus runtime, security layer versus application layer.
  • YAML is particularly rich. The combination of optional duplicate-key rejection, global/local tag semantics, Base64 key decoding, and lenient Base64 parsing creates a multi-dimensional space for language-specific divergence.
  • Differentials can be enablers, not just vulnerabilities. In the GitLab case, the Ruby/Go YAML split was not itself exploitable — it bypassed a security control to expose an existing path traversal. Hunting for differentials in isolation misses this chained impact.
  • Complexity is the enemy. Every increment in format specification complexity is potential surface for parser disagreement. Formats without mechanized, unambiguous definitions are permanently at risk of this bug class.

Reviews

Dr. Zero (Offensive Security Researcher) — STRONG ACCEPT

Joernchen presents parser differentials as a stockpilable offensive asset class rather than isolated bugs, working through CouchDB JSON, Kubernetes JWT header ordering, a three-language YAML polyglot, and a real GitLab Workspaces file write. The 'stockpile first, deploy later' framing is the conceptual contribution; the YAML three-way split (Go/Python/Ruby simultaneously seeing different key values) is the technical centerpiece. Clean examples, accessible format, genuine impact — docked one point for being an OffensiveCon talk that could have been a DEF CON talk.

Heather Calloway (CISO) — SOLID

Joernchen's parser differential research spans CouchDB RCE, JWT header direction attacks, YAML polyglots across three languages, and a GitLab Workspaces path traversal where a security control was completely bypassed because the Ruby and Go components of the same system parsed the same document differently.

→ Top-rated talks at OffensiveCon 2025

All talks from OffensiveCon 2025