Building an XSW Honeypot Endpoint: Catching SAML Signature Wrapping Attacks in the Wild

    XSW Attack?

    Building an XSW Honeypot Endpoint: Catching SAML Signature Wrapping Attacks in the Wild

    Most security teams know SAML is involved in their SSO stack. Far fewer have ever looked closely at what's actually inside those XML payloads, or what happens when an attacker decides to start messing with them.

    XML Signature Wrapping — XSW — is one of those attacks that sounds academic until the day you realize your identity provider's signature is valid, your service provider accepted the response, and the authenticated user is someone who should never have gotten in. The attack exploits a gap between what gets cryptographically signed and what the application actually reads. The signature checks out. The assertion is a lie.

    So we built a trap. The XSW Honeypot Endpoint is a fake SAML ACS (Assertion Consumer Service) that deliberately accepts everything — no signature validation, no rejection — and in return, logs every payload in full, runs it through a detection engine that identifies which of the eight known XSW variants are present, and fires an alert to your security team in real time. Attackers think they're probing a live endpoint. You're watching every move.

    Here's how we built it, layer by layer.

    FIRST — UNDERSTAND WHAT XSW ACTUALLY DOES

    When a user authenticates via SAML, the Identity Provider (IdP) sends a digitally signed XML response to the Service Provider (SP) at its ACS endpoint. The SP is supposed to validate that signature and then read the assertion to know who the user is. XSW attacks exploit the fact that XML parsers and XML signature validators often don't agree on which element they're looking at.

    An attacker intercepts a legitimate SAML response, wraps a cloned, malicious assertion around (or next to) the real signed one, and sends it in. The signature validator looks at the genuine element and says everything checks out. The application reads the malicious element and grants access. Eight documented variants of this technique exist — XSW1 through XSW8 — each exploiting a different structural quirk in how XML is parsed. For example: XSW1 buries the signed assertion inside an unsigned clone; XSW7 hides the malicious assertion inside a ds:KeyInfo element; XSW8 wraps the whole legitimate payload inside a ds:Object tag.

    The honeypot's job is to collect these payloads before they ever reach your real SP — and to learn from them.

    THE ARCHITECTURE — HOW DATA FLOWS THROUGH THE SYSTEM

    Before writing a single line, we mapped the flow end to end. An attacker POSTs a crafted SAML response to /saml/acs. The honeypot decodes it from Base64, parses the XML, runs it through the XSW detector, persists it to Redis and disk, fires an alert webhook, and returns a convincing 302 redirect — so the attacker has no indication they've hit a trap.

    architecture
    POST /saml/acs  ← Attacker sends crafted SAML response here
           ↓
      Base64 decode + XML parse
           ↓
      XSW Detector (XSW1–XSW8 + structural analysis)
           ↓
      Persist to Redis + disk
           ↓
      Alert webhook (Slack / Discord / custom)
           ↓
      Realistic 302 redirect (attacker stays unaware)

    The project structure reflects this pipeline cleanly:

    directory
    xsw_honeypot/
    ├── app/
    │   ├── main.py               # FastAPI app + lifespan
    │   ├── config.py             # Pydantic settings
    │   ├── models.py             # Data models (SAMLCapture, XSWAnalysis, etc.)
    │   ├── routers/
    │   │   ├── honeypot.py       # Fake ACS endpoint (POST /saml/acs)
    │   │   └── dashboard.py      # Dashboard API (/api/stats, /api/captures)
    │   ├── services/
    │   │   ├── xsw_detector.py   # Core XSW detection engine
    │   │   ├── saml_parser.py    # Best-effort SAML XML parser
    │   │   ├── storage.py        # Redis + disk persistence
    │   │   └── alerting.py       # Slack / Discord / webhook alerts
    │   └── templates/
    │       └── dashboard.html    # Single-page monitoring dashboard
    ├── tests/
    │   └── test_xsw_detector.py  # Full test suite
    ├── requirements.txt
    └── README.md

    STEP 1 — INSTALL DEPENDENCIES AND CONFIGURE THE ENVIRONMENT

    Clone the repo and install:

    bash
    $ git clone https://github.com/SpeechieX/xsw-honeypot-endpoint.git
    $ cd xsw-honeypot-endpoint
    $ pip install -r requirements.txt

    All behavior is driven by environment variables. Drop a .env file in the root — no hardcoded config anywhere. The key variables you'll want to set immediately:

    .env
    ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
    ALERT_ON_XSW_ONLY=true
    ALERT_MIN_SEVERITY=20
    DASHBOARD_USERNAME=admin
    DASHBOARD_PASSWORD=changeme
    PERSIST_TO_DISK=true

    ALERT_ON_XSW_ONLY=true means you only get paged when something suspicious is actually detected — not for every benign probe. ALERT_MIN_SEVERITY gives you a noise floor; anything scoring under 20 out of 100 is silently logged but won't wake you up. Both settings are tunable as you learn the traffic patterns hitting your honeypot.

    STEP 2 — THE FAKE ACS ENDPOINT (honeypot.py)

    The core of the honeypot is the fake ACS endpoint defined in honeypot.py. It's intentionally permissive — it accepts every POST regardless of what's in the payload, which is the entire point. A real SP would reject malformed or unsigned responses immediately. Ours welcomes them.

    On every incoming request, the endpoint: extracts the raw SAMLResponse field from the POST body, decodes it from Base64, attempts to parse the XML, runs the XSW detector against the parsed tree, hands the result to the storage and alerting layers, and returns a 302 redirect to a plausible destination. That final redirect is critical — it keeps the attacker in the dark and makes the endpoint behave like a real SP would after a successful authentication.

    STEP 3 — BUILDING THE XSW DETECTION ENGINE (xsw_detector.py)

    This is the heart of the project. The detector takes a parsed XML tree and runs a series of structural checks against it — one for each of the eight known XSW variants. Each check looks for the specific XML topology that variant uses to smuggle a malicious assertion past signature validation.

    Here's a breakdown of what each variant does and what we look for:

    xsw variant reference
    XSW1 — Signed assertion buried inside unsigned clone
    XSW2 — Unsigned clone precedes signed assertion as sibling
    XSW3 — Signed element wrapped in new unsigned parent
    XSW4 — Unsigned sibling shadows signed element
    XSW5 — Extensions block used to hide signed assertion
    XSW6 — Signature element relocated into cloned assertion
    XSW7 — Malicious assertion nested inside ds:KeyInfo
    XSW8 — ds:Object element wraps the legitimate content

    Each check returns a result with an XPath location pointing to exactly where the suspicious structure was found, a confidence score, and a variant label. The overall capture gets a severity score from 0–100 based on how many indicators fired and how confident the detector is in each one. That score is what flows through to the alerting layer.

    STEP 4 — SAML PARSING (saml_parser.py)

    One of the practical challenges we ran into early: SAML responses in the wild are messy. Attackers deliberately malform them. Namespaces are inconsistent. Some payloads won't parse cleanly at all. The saml_parser.py module handles this with a best-effort approach — it extracts what it can (issuer, subject, conditions, attributes, assertion IDs) and flags anything it couldn't parse rather than throwing an exception and dropping the capture.

    The rule is: never discard a payload because it's malformed. A malformed payload is often the most interesting one.

    STEP 5 — PERSISTENCE WITH REDIS AND DISK (storage.py)

    Every capture is persisted in two places. Redis gives you fast reads for the dashboard and querying. Disk storage (as .jsonl files in the captures/ directory) gives you a durable audit trail you can grep, archive, or feed into other tooling. Redis is optional — if it's not running, the honeypot falls back to disk only. Start it with Docker in one line:

    bash
    $ docker run -d -p 6379:6379 redis:alpine

    Each capture record stores the raw XML payload, the full parsed SAML fields, the XSW analysis results (including XPath locations for every indicator), the source IP, timestamps, severity score, and the variant guesses. Treat the captures/ directory as sensitive — raw attacker payloads live there.

    STEP 6 — REAL-TIME ALERTING (alerting.py)

    When a capture meets the threshold — either any capture (if ALERT_ON_XSW_ONLY=false) or a detected XSW attempt above the severity floor — the alerting module fires a webhook to Slack, Discord, or any custom HTTP endpoint. The alert payload includes the source IP, timestamp, severity score, and which XSW variants were flagged. Your team gets the signal in real time, with enough context to act immediately.

    STEP 7 — THE MONITORING DASHBOARD

    Once the server is running, point your browser to http://localhost:8000/dashboard. The single-page dashboard auto-refreshes every 30 seconds and gives you a live view of total captures, XSW suspected count, unique IPs, and severity breakdown. Drill into any individual capture to see the raw XML, the parsed SAML fields, and a full breakdown of every XSW indicator that fired — including the exact XPath location of the suspicious structure in the document.

    Lock it down with the basic auth env vars before exposing it anywhere outside localhost.

    RUNNING THE FULL STACK

    bash
    # 1. Install dependencies
    $ pip install -r requirements.txt
    
    # 2. (Optional) Start Redis for persistent storage
    $ docker run -d -p 6379:6379 redis:alpine
    
    # 3. Run the honeypot
    $ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    
    # 4. Open the dashboard
    $ open http://localhost:8000/dashboard

    RUNNING THE TEST SUITE

    The detection engine has a full test suite in test_xsw_detector.py covering all eight variants plus structural edge cases. Run it before deploying any changes to the detector:

    bash
    $ pip install pytest
    $ pytest tests/ -v

    WHY BUILD THIS INSTEAD OF JUST PATCHING?

    Patching your SP to enforce strict signature validation is necessary — but it's not sufficient on its own. Once you've patched, you stop seeing attacks. You stop learning about the tooling attackers are using, the variant patterns they favor, the IPs and timing of probes. A honeypot lets you stay blind to nothing while exposing nothing real.

    Run this in front of your real ACS endpoint — or beside it as a decoy on a registered subdomain — and you get an ongoing intelligence feed on who's testing SAML exploits against your infrastructure, and exactly how they're doing it. That's the kind of visibility that turns a reactive security posture into a proactive one.

    The full source is on GitHub: github.com/SpeechieX/xsw-honeypot-endpoint

    Erik HR is a software engineer and creative developer originally from Detroit, MI.
    Erik HR is a software engineer, writer, visualist, and creative currently living in various countries in SE Asia. For inquiries, please write to hello@erick-robertson.com.