SOAP and XML APIs
How SOAP envelopes, WSDL contracts, and WS-Security actually work on the wire, why XML parsing makes SOAP a rich attack surface, and the signature-wrapping, SOAPAction, and entity bugs that still live in enterprise systems.
SOAP (Simple Object Access Protocol) predates REST and still runs much of the enterprise world: banking cores, payment gateways, telecom provisioning, government systems, healthcare (HL7 over SOAP), and the integration middleware that glues them together. You will not often build a new SOAP service, but you will absolutely meet one in an assessment, and it behaves nothing like the JSON APIs on the rest of this site. Everything is XML, the contract is machine-readable and usually public, and the security model is a stack of XML standards that are hard to implement correctly. That combination is why SOAP endpoints reward careful testing.
The envelope
Every SOAP message, request or response, is an XML document wrapped in an Envelope with an optional Header and a mandatory Body. A call looks like this:
POST /BankService HTTP/1.1
Host: api.example.test
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:example:getBalance"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<auth:Credentials xmlns:auth="urn:example:auth">
<auth:Token>eyJhbGci...</auth:Token>
</auth:Credentials>
</soap:Header>
<soap:Body>
<bank:getBalance xmlns:bank="urn:example:bank">
<bank:accountId>4021</bank:accountId>
</bank:getBalance>
</soap:Body>
</soap:Envelope>
The Header carries cross-cutting concerns: authentication tokens, routing, transaction ids, and the WS-* security material described below. The Body carries the actual operation and its arguments. A failure comes back not as an HTTP status but as a <soap:Fault> element inside a 200 response, which is the first thing that trips people used to REST: a SOAP call can “succeed” at the HTTP layer and still be a detailed error, often one that leaks stack traces, internal class names, or database messages straight out of the fault string.
The transport is almost always HTTP, but the SOAP layer is deliberately transport-agnostic (the same envelope can ride JMS or SMTP), which is why it reinvents addressing, security, and error handling instead of using HTTP’s. That reinvention is the attack surface.
SOAPAction
Notice the SOAPAction HTTP header. It names the operation being invoked, separately from the Body that also names it. When a server dispatches on SOAPAction while authorization or filtering keys off the Body (or a gateway inspects one and the backend honors the other), the two can be made to disagree. SOAPAction spoofing sends one operation in the header and another in the Body to slip past a filter that only reads the header, the same parser-disagreement class as request smuggling and the X-User-Id gateway trust problem in services and trust. Always test whether the header or the Body actually decides what runs.
WSDL is the whole map
A SOAP service is described by a WSDL (Web Services Description Language) document, itself XML, that lists every operation, every message’s exact schema, the data types, the endpoint URLs, and the bindings. It is the OpenAPI of the SOAP world, except it is older, more verbose, and far more likely to be exposed without anyone realizing it.
Find it by appending ?wsdl to the endpoint (https://api.example.test/BankService?wsdl), or at .asmx?wsdl, ?singleWsdl, or a /services listing on older .NET and Java stacks. Once you have the WSDL, a tool like SoapUI, or the zeep library in Python, parses it and generates a client for every operation, including the ones no UI ever calls. The WSDL frequently reveals administrative or internal operations (resetPassword, getAllUsers, executeQuery) that exist on the endpoint but were never meant to be reachable, which is the SOAP version of hidden-endpoint discovery. Enumerating and testing every operation the WSDL lists, not just the ones the application uses, is the core of SOAP recon.
WS-Security
SOAP does not lean on TLS and cookies for security. It has its own standard, WS-Security, that puts the security material inside the SOAP Header:
- UsernameToken: a username with either a plaintext password or a
PasswordDigest(a SHA-1 of a nonce, a timestamp, and the password). The digest form was meant to avoid sending the password, but it requires the server to store the password reversibly to verify it, and the SHA-1 digest is weak by modern standards. - Timestamps: a
<wsu:Timestamp>withCreatedandExpiresto bound replay. If the server does not actually enforce expiry, captured messages replay forever. - XML Signature: signs part of the message so the receiver can detect tampering.
- XML Encryption: encrypts elements (for example the password or a card number) at the message level rather than relying on transport encryption, so the ciphertext survives being routed through intermediaries.
The strength of WS-Security is message-level, end-to-end protection that survives multiple hops. The weakness is that it is genuinely hard to implement, and the most damaging SOAP bug lives exactly in how signatures are verified.
XML Signature Wrapping
XML Signature Wrapping (XSW) is the signature bug that defines SOAP security testing, the same attack covered for SAML in single sign-on because SAML assertions are signed XML too.
The flaw is a split between what was signed and what gets processed. XML Signature references the element it protects by an id (Reference URI="#body-1"). The signature verifier finds the element with that id, checks the signature over it, and reports “valid.” A separate piece of code then reads the operation, and it often locates the element by its position or name (the first <soap:Body> child) rather than by the id that was actually signed.
An attacker who captures one validly signed message can move the signed element aside (into an unused header, or a wrapper element) while keeping its original id, then inject a new, unsigned Body with the attacker’s chosen operation in the position the processor reads. The verifier still finds the original signed element by id and passes it. The business logic reads the injected element. The message is accepted as authentically signed while executing something the signer never approved, for example changing accountId from the attacker’s to the victim’s, or flipping a transfer amount.
The defense is to make verification and processing agree on the same element: verify using a schema-aware, position-sensitive approach that rejects documents where the signed element is not exactly where the application reads from, reject documents with duplicate ids or unexpected structure, and prefer libraries that process only what was signed. Hand-rolled “find the id, check it, then go read the Body” verification is the vulnerable pattern.
XML parsing bugs come free with SOAP
Because every SOAP message is XML parsed by a full XML processor, the entire family of XML parser attacks applies to every endpoint:
- XXE (XML External Entity): if the parser resolves external entities, a crafted envelope reads local files (
file:///etc/passwd), reaches internal services (SSRF against cloud metadata), or exfiltrates data out of band. SOAP’s XML-everything design means XXE is in scope for every operation, not just an occasional endpoint that happens to accept XML. See the XXE section of injection and file attacks. - Billion laughs / entity expansion: nested entity definitions expand a tiny document into gigabytes and exhaust memory, a denial of service, which connects to the parser-exhaustion theme in algorithms for security.
- DTD and parameter entities: even with external entities disabled, an enabled DTD can enable blind XXE variants and expansion attacks. Disabling DTD processing entirely is the blunt, correct fix.
- Injection into the Body: operation arguments flow into the same SQL, command, and path sinks as any other input. The XML wrapper changes nothing about what the backend does with the value once parsed.
The single most important server-side setting for a SOAP stack is a hardened XML parser: external entities off, DTD processing off, and entity expansion bounded.
Testing a SOAP service
- Get the WSDL. Try
?wsdl,?singleWsdl,.asmx?wsdl, and service-listing paths. Without it you can still test, but with it you have every operation and schema. - Generate a client. Load the WSDL into SoapUI or Python’s
zeep, which gives you a working call for every operation, including undocumented ones. - Enumerate operations. Call operations the application never uses and check whether authorization is enforced per operation or only at the front door. Missing function-level authorization (BFLA) is as common here as in REST.
- Probe the XML parser. Test each operation for XXE and entity expansion with a safe out-of-band canary; treat these as present until proven absent.
- Attack the signatures. Where WS-Security signatures are used, test XSW: relocate the signed element and inject an unsigned one, and test whether timestamps and nonces are actually enforced against replay.
- Spoof SOAPAction. Send mismatched
SOAPActionand Body to find dispatch-versus-authorization disagreements. - Read the faults. Trigger errors and harvest the
<soap:Fault>contents for stack traces, internal types, and backend messages.
As always, only test services you own or are explicitly authorized to assess. SOAP endpoints are often load-bearing production integrations, and entity-expansion and WSDL-enumeration tests in particular can be disruptive, so scope and rate-limit them deliberately.
Field notes: tooling that does the heavy lifting
You do not hand-write SOAP envelopes. You generate a client from the WSDL and let it build every call:
# zeep parses the WSDL and exposes every operation as a method
from zeep import Client
client = Client("https://api.example.test/BankService?wsdl")
print(client.wsdl.dump()) # every operation, type, and binding
client.service.getBalance(accountId=4021)
SoapUI does the same with a GUI, generates sample requests for every operation, and its security-scan features fuzz for SQL injection, XXE, and malformed-XML handling across the whole service in one pass. For the signature attacks, SAML Raider and WS-Attacker automate the XSW permutations so you are not relocating elements by hand. Burp with the Content Type Converter extension flips a JSON API to XML to test whether a dual-format endpoint exposes a weaker XML parser (and XXE) that the JSON path hid.
A fast triage once you have the WSDL: enumerate every operation, call each one unauthenticated and as a low-privilege user to find missing function-level authorization, fire a single out-of-band XXE canary at each to find the parsers that resolve entities, and read every <soap:Fault> for leaked internals. Only against services in your authorized scope, and throttle the entity and WSDL-enumeration tests because SOAP endpoints are often fragile production integrations.
Takeaway
SOAP is verbose and old, but it is not going away, and it concentrates a lot of security in a few hard-to-implement XML standards. The map is handed to you in the WSDL, the authentication lives in the Header, and the highest-impact bugs come from an XML parser that trusts too much or a signature check that verifies one element while the application reads another. Learn to read the envelope and the WSDL, and a SOAP endpoint becomes as explorable as any REST API. For the JSON-era styles, see REST and GraphQL; for the standards SOAP reuses, see XML signature wrapping in SSO and XXE.