16th of February, 2026
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.
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.