Security¶
soapbar uses a hardened lxml parser:
lxml.etree.XMLParser(
resolve_entities=False, # XXE prevention
no_network=True, # SSRF prevention
load_dtd=False, # DTD injection prevention
huge_tree=False, # Billion-Laughs prevention
remove_comments=True, # comment injection prevention
remove_pis=True,
)
Entity references (potential XXE payloads) are silently dropped rather than expanded. No network connections are made during parsing. DTDs are not loaded.
The hardened parser has two public entry points: parse_xml(data) takes
str | bytes, and parse_xml_document(source) is its polymorphic variant
accepting str | bytes | Path | _Element (a Path is read from disk; an
already-parsed element passes through unchanged). Both apply the exact
configuration above; parse_xml_document is what parse_wsdl itself uses to
load documents and resolve imports. Use either instead of a raw
lxml.etree.fromstring whenever the input is not fully trusted.
Additional hardening:
- SSRF guard on WSDL imports:
wsdl:import/xsd:importlocations are not fetched by default — remote (http(s)://) imports requireparse_wsdl(..., allow_remote_imports=True)and local-file imports requireallow_local_imports=True, so a hostile WSDL cannot reach internal hosts or read local files. - Message size limit:
SoapApplication(max_body_size=10*1024*1024)— requests exceeding 10 MB are rejected with aClientfault before XML parsing. - XML nesting depth: requests exceeding 100 levels of nesting are rejected to prevent stack exhaustion.
- Error scrubbing: unhandled exceptions produce
"An internal error occurred."— no stack traces or exception text are returned to clients. - HTTPS warning:
SoapApplicationwarns at construction time ifservice_urluses plain HTTP.
Client DEBUG logging and credentials¶
soapbar.client.client emits request and response envelopes at DEBUG
(soapbar.client.transport logs only transport-level events). Nothing is logged
unless you opt in, and the envelopes are redacted before they reach a log record:
wsse:Securityheader blocks are emptied, so aUsernameTokenpassword never appears.- Elements whose local name names a secret —
Password,Senha,Secretand similar, in any namespace — have their text replaced wherever they appear, including the Body. Not every service carries credentials in the WS-Security header:soapbar.contrib.ana's CotaOnline operations authenticate withLogin/Senhaas plain Body elements. - Usernames are deliberately not redacted. Knowing which account was used is most of a debug log's value, and a login is not a credential on its own.
- Bodies that are not parseable XML — an MTOM multipart payload, a proxy's HTML
error page — are described (
<N bytes, not parseable as XML …>) rather than dumped, and every logged envelope is capped at 8192 characters.
Redaction is best-effort
soapbar cannot know the element names of every service it talks to. If your
service carries a secret in a field this list does not name, that field will
be logged. Treat DEBUG on the client as something you turn on deliberately,
for a bounded period, against a log sink you control — not as a level to leave
enabled in production.
Security assurance¶
These protections are verified continuously, not just designed in:
- Static analysis — CodeQL with the
security-extendedquery pack, plus ruff's flake8-bandit rules and strict mypy, run on every push and pull request. - Fuzzing — a coverage-guided Atheris/libFuzzer harness feeds arbitrary bytes into every parser that accepts untrusted input (
parse_xml,check_xml_depth,SoapEnvelope.from_xml,parse_wsdl) on a weekly schedule; any exception outside the documented error contract counts as a crash. - Property-based tests — Hypothesis properties assert round-trip and robustness invariants for the XSD type system, envelope serialisation, and the parsers, as part of the normal test suite.
- OpenSSF — soapbar holds the OpenSSF Best Practices passing badge and is monitored by Scorecard.
- Adversarial SSRF corpus —
tests/audit/test_ssrf_corpus.pythrows seventeen import-resolution vectors (remote, transitive, entity/DTD, traversal, redirect) at the WSDL parser and asserts on a sentinel server's request log rather than on exceptions — a silently bypassed guard produces traffic even when nothing raises, so traffic is what the tests measure.
Verifying a release¶
Every release is signed with Sigstore keyless
signing and ships a CycloneDX SBOM. Each GitHub release
carries four assets: the wheel, the sdist, soapbar-<version>.cdx.json (the
SBOM), and soapbar-<version>-provenance.sigstore.json (the signature bundle).
There is no public key to fetch. Signing is keyless: the signing identity is the GitHub Actions workflow itself, and the certificate is issued at build time via OIDC and recorded in the public Sigstore transparency log. To verify, you assert which workflow in which repository you expect:
# Download the artifact and verify it was built by this repo's release workflow
gh release download v0.20.0 --repo hitoshyamamoto/soapbar --pattern '*.whl'
gh attestation verify soapbar-0.20.0-py3-none-any.whl \
--repo hitoshyamamoto/soapbar \
--signer-workflow hitoshyamamoto/soapbar/.github/workflows/release.yml
A successful run confirms the artifact was produced by that workflow, from this repository, and has not been modified since. The SBOM is signed by the same attestation, so it can be verified the same way.
Packages installed from PyPI carry PEP 740
attestations published by the same pipeline; PyPI displays and verifies these
automatically, and they are visible via the /integrity/ endpoint for each file.
Reporting a vulnerability¶
Do not open a public issue for security vulnerabilities. Use the private disclosure process in the security policy — a GitHub private security advisory visible only to you and the maintainer. As a single-maintainer project the targets are acknowledgement within 5 business days and an initial assessment within 14 days, with fix timelines agreed with the reporter by severity.
WS-Security — UsernameToken¶
soapbar supports WS-Security 1.0 UsernameToken (OASIS 2004), both plain-text and SHA-1 digest.
Client — attaching credentials¶
from soapbar import SoapClient
from soapbar.core.wssecurity import UsernameTokenCredential
# Plain-text password
cred = UsernameTokenCredential(username="alice", password="secret")
# SHA-1 PasswordDigest (recommended for non-TLS scenarios)
cred = UsernameTokenCredential(username="alice", password="secret", use_digest=True)
client = SoapClient.manual(
"https://example.com/soap",
wss_credential=cred,
)
result = client.call("GetData", id=42)
The wsse:Security header is injected automatically on every call.
Server — validating credentials¶
from soapbar import SoapApplication
from soapbar.core.wssecurity import UsernameTokenValidator, SecurityValidationError
class MyValidator(UsernameTokenValidator):
_users = {"alice": "secret", "bob": "hunter2"}
def get_password(self, username: str) -> str | None:
return self._users.get(username)
app = SoapApplication(
service_url="https://example.com/soap",
security_validator=MyValidator(),
)
app.register(MyService())
SecurityValidationError is converted to a Client SOAP fault automatically. Both PasswordText and PasswordDigest token types are verified; Digest requires wsse:Nonce and wsu:Created to be present.
XML Signature and Encryption¶
Requires pip install soapbar[security] (pulls in signxml and cryptography).
XML Digital Signature (XML-DSIG)¶
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.x509 import CertificateBuilder
from soapbar.core.wssecurity import sign_envelope, verify_envelope, XmlSecurityError
# Sign — enveloped RSA-SHA256 XML-DSIG
signed_bytes = sign_envelope(envelope_bytes, private_key, certificate)
# Verify — raises XmlSecurityError on bad signature
try:
verified_bytes = verify_envelope(signed_bytes, certificate)
except XmlSecurityError as exc:
print("Signature invalid:", exc)
Signing an internal element by Id¶
Some services sign an inner element selected by its Id (referenced as
#<id>) rather than the whole envelope — most notably SEFAZ NF-e, which signs
<infNFe>. sign_element_by_id does exactly that, with a single
ds:Reference and an enveloped signature:
from soapbar.core.wssecurity import sign_element_by_id
# Defaults: RSA-SHA256 / SHA-256 / Exclusive C14N.
signed = sign_element_by_id(nfe_xml, "NFe3106...", private_key, certificate)
# SEFAZ NF-e mandates the legacy algorithm set:
signed = sign_element_by_id(
nfe_xml,
"NFe3106...", # the <infNFe Id="..."> value
private_key,
certificate,
signature_method="rsa-sha1",
digest_method="sha1",
c14n="inclusive", # http://www.w3.org/TR/2001/REC-xml-c14n-20010315
end_cert_only=True, # only the end-entity cert in KeyInfo
)
XML Encryption (AES-256-GCM + RSA-OAEP)¶
from soapbar.core.wssecurity import encrypt_body, decrypt_body, XmlSecurityError
# Encrypt SOAP Body — AES-256-GCM session key wrapped with recipient's RSA public key
encrypted_bytes = encrypt_body(envelope_bytes, recipient_public_key)
# Decrypt — extracts and unwraps the session key, restores Body children
decrypted_bytes = decrypt_body(encrypted_bytes, recipient_private_key)
The xenc:EncryptedData element is placed as the sole child of <soap:Body>. The body is encrypted with AES-256-GCM (XML-Enc 1.1, authenticated: the 16-byte GCM tag detects tampering), and the AES-256 session key is wrapped with RSA-OAEP (SHA-256) in an xenc:EncryptedKey element inside xenc:KeyInfo. Decryption accepts GCM by default; legacy unauthenticated AES-256-CBC ciphertext is rejected unless explicitly opted in via decrypt_body(..., allow_unauthenticated_cbc=True).
WS-I BSP X.509 Token Profile (S10)¶
For interoperability with WS-I Basic Security Profile 1.1 compliant clients and servers, use the BSP variant which embeds the certificate as a wsse:BinarySecurityToken and references it from ds:Signature/ds:KeyInfo:
from soapbar.core.wssecurity import (
sign_envelope_bsp,
verify_envelope_bsp,
build_binary_security_token,
extract_certificate_from_security,
)
# Sign — adds wsse:BinarySecurityToken + wsse:SecurityTokenReference in KeyInfo
signed_bytes = sign_envelope_bsp(envelope_bytes, private_key, certificate)
# Verify — extracts cert from BST, verifies ds:Signature
verified_bytes = verify_envelope_bsp(signed_bytes)
# Build a standalone BinarySecurityToken element (e.g. to add to an existing header)
bst = build_binary_security_token(certificate, token_id="MyToken-1")
MTOM/XOP¶
soapbar supports MTOM (Message Transmission Optimization Mechanism, W3C) for sending and receiving SOAP messages with binary attachments. The multipart/related MIME packaging is handled transparently — the core envelope sees resolved base64 data; your service code sees plain bytes.
For inspection before resolution, extract_xop_elements(xml_bytes) returns
the (parent_tag, xop_include_element) pairs found in a document — which
elements reference which attachments — without touching the attachment data.
The transparent path never needs it; it exists for tooling that wants to look
at XOP references before (or instead of) resolving them.
Client — sending attachments¶
from soapbar import SoapClient, BindingStyle
client = SoapClient.manual(
"http://localhost:8000/soap",
binding_style=BindingStyle.DOCUMENT_LITERAL_WRAPPED,
use_mtom=True,
)
# Queue a binary attachment and get its Content-ID back
cid = client.add_attachment(b"\x89PNG...", content_type="image/png")
# The call packages the envelope + attachments as multipart/related
result = client.call("UploadImage", image_cid=cid, filename="logo.png")
Server — receiving MTOM¶
No configuration required. AsgiSoapApp and WsgiSoapApp automatically detect inbound multipart/related requests, resolve all xop:Include references inline, and pass the reconstructed XML to the dispatcher as a normal SOAP envelope.
Low-level API¶
from soapbar import parse_mtom, build_mtom, MtomAttachment
# Parse a raw MTOM HTTP body
msg = parse_mtom(raw_bytes, content_type_header)
print(msg.soap_xml) # bytes — envelope with XOP includes resolved
print(msg.attachments) # list[MtomAttachment]
# Build a MTOM HTTP body
attachments = [MtomAttachment(content_id="part1@host", content_type="image/png", data=png_bytes)]
body_bytes, content_type = build_mtom(soap_xml_bytes, attachments)