backend / protocols / soap / 03_ws_security.md

WS-Security

6 interview angles 6 min read source

WS-Security

SOAP’s security framework. Adds message-level signing and encryption on top of (or instead of) transport-level TLS. Used heavily in banking, government, healthcare — anywhere “TLS is not enough” or “the message must be auditable end-to-end.”

Most Python developers consuming SOAP services only need to know: username token (basic), signed timestamps, occasionally signed bodies.

What WS-Security adds beyond TLS

TLS WS-Security
Where transport layer message body (XML signature/encryption)
Scope point-to-point end-to-end through intermediaries
Protects wire transmission the XML message itself
Headers vs body covers both configurable per element
Persistence gone once decrypted embedded in the message; can be audit-logged
Required for most internet traffic regulated industries / multi-hop systems

The use case: messages traverse multiple intermediaries (gateways, message brokers, internal proxies). TLS terminates at each hop; WS-Security stays attached to the message body itself.

The Security header

<soap:Envelope ...>
  <soap:Header>
    <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                   soap:mustUnderstand="1">
      <!-- Username token, signature, encryption, timestamp go here -->
    </wsse:Security>
  </soap:Header>
  <soap:Body>...</soap:Body>
</soap:Envelope>

soap:mustUnderstand="1" tells the receiver: “you must handle this header; if you can’t, fail.” Standard for security headers.

Username Token — the simplest

For basic-auth-like credentials inside the message:

<wsse:Security>
  <wsse:UsernameToken>
    <wsse:Username>alice</wsse:Username>
    <wsse:Password Type="...PasswordText">secret123</wsse:Password>
    <wsse:Nonce>random-bytes</wsse:Nonce>
    <wsu:Created>2024-01-15T10:30:00Z</wsu:Created>
  </wsse:UsernameToken>
</wsse:Security>

Two password types:

Type Meaning
PasswordText plaintext (under TLS or you’re in trouble)
PasswordDigest SHA1(nonce + created + password)

PasswordDigest is the “hashed” form but uses SHA1 — broken cryptographically. Modern services use TLS + PasswordText, or stronger tokens (X.509, SAML).

Server validates: username/password match, nonce hasn’t been seen recently (replay prevention), Created timestamp is fresh (clock skew window).

Timestamps

<wsse:Security>
  <wsu:Timestamp xmlns:wsu="...wsutility">
    <wsu:Created>2024-01-15T10:30:00Z</wsu:Created>
    <wsu:Expires>2024-01-15T10:35:00Z</wsu:Expires>
  </wsu:Timestamp>
</wsse:Security>

5-minute window. Receiver rejects messages outside it (anti-replay). Combined with nonce tracking for stronger replay protection.

XML Signature

For integrity + non-repudiation, sign specific elements:

<wsse:Security>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <ds:Reference URI="#body-id">
        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <ds:DigestValue>...</ds:DigestValue>
      </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>...</ds:SignatureValue>
    <ds:KeyInfo>
      <wsse:SecurityTokenReference>
        <wsse:Reference URI="#cert-id"/>
      </wsse:SecurityTokenReference>
    </ds:KeyInfo>
  </ds:Signature>
  <wsse:BinarySecurityToken wsu:Id="cert-id" EncodingType="...Base64Binary"
                            ValueType="...X509v3">MIIB...</wsse:BinarySecurityToken>
</wsse:Security>

<soap:Body wsu:Id="body-id">...</soap:Body>

The signature is over the canonicalized form of the element with wsu:Id="body-id". Steps to verify:

  1. Canonicalize the referenced element (apply exclusive canonicalization).
  2. Hash with the digest method (SHA256 here).
  3. Compare to DigestValue.
  4. Canonicalize SignedInfo.
  5. Verify signature with the public key from BinarySecurityToken.

XML Signature is famous for signature wrapping (XSW) vulnerabilities — same class as SAML’s XSW. See ../../11_authentication/sso/08_attack_vectors.md.

XML Encryption

For confidentiality of specific elements:

<soap:Body>
  <xenc:EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Content">
    <xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/>
    <ds:KeyInfo>
      <xenc:EncryptedKey>...</xenc:EncryptedKey>
    </ds:KeyInfo>
    <xenc:CipherData>
      <xenc:CipherValue>...</xenc:CipherValue>
    </xenc:CipherData>
  </xenc:EncryptedData>
</soap:Body>

The plaintext body element is replaced by <EncryptedData>. Sender encrypts with a fresh symmetric key; encrypts that key with the recipient’s RSA public key; bundles both.

Recipient: decrypts the key with their private key, then decrypts the body.

X.509 Token authentication

Instead of username/password, present a certificate:

<wsse:Security>
  <wsse:BinarySecurityToken wsu:Id="my-cert"
                            EncodingType="...Base64Binary"
                            ValueType="...X509v3">MIIB...base64 cert...</wsse:BinarySecurityToken>
  <ds:Signature>...signed by the cert's private key...</ds:Signature>
</wsse:Security>

The signature proves possession of the cert’s private key — authenticates the sender.

Trust model: the receiver has a CA that signed acceptable client certs (or pinned client cert thumbprints).

This is mTLS-equivalent at the message layer.

SAML token in SOAP

For federated identity in SOAP services:

<wsse:Security>
  <saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
                   Version="2.0" ID="..." IssueInstant="...">
    <saml2:Issuer>https://idp.example.com</saml2:Issuer>
    <ds:Signature>...</ds:Signature>
    <saml2:Subject>
      <saml2:NameID>alice@example.com</saml2:NameID>
    </saml2:Subject>
    <saml2:AttributeStatement>...</saml2:AttributeStatement>
  </saml2:Assertion>
</wsse:Security>

The SAML assertion is the same as in SSO — see ../../11_authentication/sso/01_saml_deep_dive.md. It carries identity claims from an IdP into the SOAP service.

Common WS-Security patterns

Different industries use different combinations:

Pattern Use case
Username token over TLS basic enterprise B2B
Signed timestamp + body financial APIs (audit trail)
Encrypted body + signature end-to-end through intermediaries
X.509 token + signature machine-to-machine PKI
SAML token federated identity (between organizations)

The combination matters — “signed but not encrypted” lets intermediaries read but not modify.

Attacks specific to WS-Security

XML Signature Wrapping (XSW)

Same idea as SAML XSW. The attacker rearranges the XML so the signature validates against one element but the application processes a different one.

<soap:Body>
  <usr:GetUser wsu:Id="signed">
    <usr:Id>42</usr:Id>           <!-- signed; parser ignores -->
  </usr:GetUser>
  <usr:GetUser>
    <usr:Id>1</usr:Id>             <!-- NOT signed; parser uses -->
  </usr:GetUser>
</soap:Body>

Defense: same-element validation (the parser must use exactly what was signed), schema-strict parsing, careful library choice.

XXE — XML External Entity

Not WS-Security-specific, but every SOAP service is vulnerable if not configured carefully:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<soap:Envelope>
  <soap:Body>
    <usr:GetUser><usr:Id>&xxe;</usr:Id></usr:GetUser>
  </soap:Body>
</soap:Envelope>

The XML parser fetches file:///etc/passwd and substitutes its contents. Defense: disable DTDs and external entities in your XML parser. Modern libraries do this by default; older ones don’t.

In Python:

import lxml.etree
parser = lxml.etree.XMLParser(resolve_entities=False, no_network=True)

Replay attacks

Capture a signed/encrypted message; resend it. Defense: timestamps + nonce tracking (server remembers recently-seen nonces for the timestamp window).

Signature stripping

Attacker removes the signature header; if the server doesn’t require it, the message is processed unsigned. Defense: mustUnderstand="1" on the Security header; server policy rejects messages without signatures.

When WS-Security matters in practice

Most Python consumers of SOAP services use:

  1. TLS + UsernameToken: 80% of cases. The Python library handles it.
  2. TLS + signed timestamp + body: regulated industries. Library config + a cert.
  3. X.509 token: rare for consumers; common for service-to-service in banks.
  4. Full WS-Security (sign + encrypt): rare; complex; usually with vendor SDK.

If you have to implement #3 or #4 from scratch, allocate significant time. The libraries help but the WS-Security stack has many footguns.

Common pitfalls

  • Trusting mustUnderstand but not validating signatures in the application — header present ≠ valid.
  • XXE not disabled in the XML parser — file read or SSRF via SOAP request.
  • Accepting any cert in WS-Security X.509 — must validate against a known CA.
  • Clock skew — strict 5-min windows fail when client/server clocks drift. Sync via NTP; allow leeway.
  • Reusing nonces — anti-replay broken.
  • Outdated SHA1 in PasswordDigest — broken cryptographically.

Common interview confusions

  • “WS-Security replaces TLS.” — they’re complementary. WS-Security is end-to-end through intermediaries; TLS is point-to-point. Use both.
  • “WS-Security is just username/password.” — it’s a framework: tokens (username, X.509, SAML), signatures, encryption, timestamps. Pick what you need.
  • “XML Signature is the same as JWT signature.” — both sign data; XML Signature is much more complex (canonicalization, references, transforms), historically with more vulnerabilities (XSW).

Interview angle

  • “What is WS-Security?” — SOAP’s security framework for message-level signing and encryption. Lives in the <wsse:Security> header. Includes tokens (username, X.509, SAML), XML Signature, XML Encryption, timestamps.
  • “Why WS-Security on top of TLS?” — TLS is point-to-point; terminates at each hop. WS-Security stays with the message through intermediaries (gateways, message brokers, audit logs). Required for multi-hop systems and regulated audit trails.
  • “What’s UsernameToken?” — basic auth equivalent for SOAP: username + password (plaintext or SHA1 digest) + nonce + timestamp in the Security header. Plaintext is fine over TLS; SHA1 digest is cryptographically broken.
  • “What’s XML Signature Wrapping?” — class of attacks where the signature validates one element but the parser processes a different one. Mitigation: same-element validation, schema-strict parsing, current libraries.
  • “What’s XXE and why does it matter for SOAP?” — XML External Entity: attacker injects an entity that the XML parser resolves, fetching local files or causing SSRF. Disable DTD processing and external entity resolution in any XML parser.
  • “Why might you sign without encrypting?” — sign for integrity + non-repudiation (audit trail showing who sent what). Encrypt for confidentiality. Many use cases want intermediaries to read messages (for routing, logging) but not modify them — signing-only matches.