You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue tracks a multi-PR initiative to make the SAML/XML parsing
surface of ex_samlauditable, hardened, and observable in a single
place, gated by a CI check before each release.
✨ Feature / Mix security.check_release task #15 drafted a mix security.check_release Mix task that scanned source
for unsafe patterns via regex. It was closed in favour of the layered
plan below, because regex source-scans are silently contournable (false
negatives are worse than no gate).
The root cause across these incidents is structural: :xmerl_scan was
called in four different files with slightly different option shapes,
error handling, and encoding conversion. Any audit or hardening had to be
done four times; any drift was invisible.
Goal
Three pillars, delivered as four PRs:
Centralisation — a single hardened entry point for XML parsing, so
any audit reduces to one grep / one AST visitor.
Audit gate — a CI-blocking check that fails the build if a new :xmerl_scan.* call appears anywhere outside the hardened wrapper.
Observability — a configurable hook on parse failures so consumers
(SIEM, OCSF, telemetry) can forward events without ex_saml owning
the formatting.
"Done" looks like: every release pipeline runs mix security.check_release
and fails fast on a new unhardened parsing site; production parse failures
surface to the consumer's SIEM via the configured hook without any code
change in ex_saml.
Core invariant: any reference to :xmerl_scan.* in lib/ outside lib/ex_saml/core/xml/safe_xml.ex is a violation.
Fail loud (non-zero exit) when a check targets a file that does not
exist — the previous draft returned [] silently, which is a false
negative.
PR 3 — Config invariants at ExSaml.SP.setup/1
Migrate the open-redirect and unsolicited-target checks from
"regex over sp_handler.ex" (which targets an app-side file not in
this lib) to structural invariants raised at init:
raise if trusted_fingerprints is :any and Mix.env() == :prod.
raise if allowed_target_urls is not set when unsolicited
IdP-initiated responses are accepted.
A raise at init is strictly more reliable than a grep — the
consumer cannot ship without seeing the failure.
PR 4 — CI wiring + ExUnit coverage of the audit task
Add a Security audit step in .github/workflows/ci.yml running mix security.check_release.
ExUnit tests for Mix.Tasks.Security.CheckRelease:
fixture "good code" → 0 violations
fixture "new unhardened :xmerl_scan call" → 1 violation
fixture "targeted file does not exist" → error, not silent zero
Wire the task into the release pipeline ahead of mix hex.publish.
Non-goals
Cryptographic verification of <ds:Signature> against a trust anchor —
lives downstream of parsing, orthogonal to this initiative.
Semantic validation of metadata structure — covered by ✨ Feature / SAML metadata validation #17 (different
layer: :xmerl_xpath on an already-parsed tree, no new :xmerl_scan
sites).
Replacing xmerl with a different XML parser.
OCSF event formatting — ex_saml exposes the :on_error hook as a
primitive; OCSF formatting belongs in a downstream consumer
(e.g. elixir-ocsf).
🔐 Security / Fix XXE, NotBefore validation, algorithm whitelist #12 (XXE + NotBefore + algorithm allow-list) — introduced the
hardened :xmerl_scan options that PR 1 now centralises. The options
themselves were already in production before this initiative; this
work hardens the structural guarantee that they cannot be bypassed
at a new call site.
👷 CI / GitHub actions security and quality checks #18 (GitHub Actions security and quality checks) — the existing CI
runs Sobelow / Credo / Dialyzer / mix_audit / hex.audit. Those
are generic. PR 4 adds the SAML-domain-aware gate that complements
them.
Context
This issue tracks a multi-PR initiative to make the SAML/XML parsing
surface of
ex_samlauditable, hardened, and observable in a singleplace, gated by a CI check before each release.
Background:
four
:xmerl_scan.stringcall sites.sites — the per-site duplication made the fix cost four files instead of
one.
mix security.check_releaseMix task that scanned sourcefor unsafe patterns via regex. It was closed in favour of the layered
plan below, because regex source-scans are silently contournable (false
negatives are worse than no gate).
The root cause across these incidents is structural:
:xmerl_scanwascalled in four different files with slightly different option shapes,
error handling, and encoding conversion. Any audit or hardening had to be
done four times; any drift was invisible.
Goal
Three pillars, delivered as four PRs:
any audit reduces to one grep / one AST visitor.
:xmerl_scan.*call appears anywhere outside the hardened wrapper.(SIEM, OCSF, telemetry) can forward events without
ex_samlowningthe formatting.
"Done" looks like: every release pipeline runs
mix security.check_releaseand fails fast on a new unhardened parsing site; production parse failures
surface to the consumer's SIEM via the configured hook without any code
change in
ex_saml.PLAN
PR 1 —
ExSaml.Core.Xml.SafeXmlwrapper + tuple API +:on_errorhook (✨ Feature / SafeXml — single hardened xmerl_scan entry point #29):xmerl_scan.string, always forcesallow_entities: false,namespace_conformant: true,quiet: true.scan/1 :: {:ok, xml()} | {:error, :invalid_xml}.Binding.decode_response/2,Sp.decrypt_assertion/2,Helper.decode_saml_payload/2.site-by-site.
config :ex_saml, :safe_xml, on_error: fn ctx -> … end— handler receives
%{reason, kind, payload}, exceptions from thehandler are swallowed.
PR 2 — AST-based audit in
mix security.check_releaseAST visitor (
Code.string_to_quoted/1).:xmerl_scan.*inlib/outsidelib/ex_saml/core/xml/safe_xml.exis a violation.exist — the previous draft returned
[]silently, which is a falsenegative.
PR 3 — Config invariants at
ExSaml.SP.setup/1"regex over
sp_handler.ex" (which targets an app-side file not inthis lib) to structural invariants raised at init:
raiseiftrusted_fingerprintsis:anyandMix.env() == :prod.raiseifallowed_target_urlsis not set when unsolicitedIdP-initiated responses are accepted.
raiseat init is strictly more reliable than a grep — theconsumer cannot ship without seeing the failure.
PR 4 — CI wiring + ExUnit coverage of the audit task
Security auditstep in.github/workflows/ci.ymlrunningmix security.check_release.Mix.Tasks.Security.CheckRelease::xmerl_scancall" → 1 violationmix hex.publish.Non-goals
<ds:Signature>against a trust anchor —lives downstream of parsing, orthogonal to this initiative.
layer:
:xmerl_xpathon an already-parsed tree, no new:xmerl_scansites).
xmerlwith a different XML parser.ex_samlexposes the:on_errorhook as aprimitive; OCSF formatting belongs in a downstream consumer
(e.g.
elixir-ocsf).Cohérence avec l'existant
✨ Feature / SAML metadata validation #17 (metadata validation, parallel initiative) — same file
(
lib/ex_saml/metadata.ex) but different layer. ✨ Feature / SAML metadata validation #17 adds semanticrules that run on the parsed
xmlElementtree; this issue makes surethat tree was produced through a hardened parser. They compose cleanly:
Metadata.validate/2callsparse/1(hardened by PR 1) → on success,runs
check_entity_descriptor/1(extended by ✨ Feature / SAML metadata validation #17). ✨ Feature / Metadata validation — certificate and ds:Signature structure rules #31 addsCertificate.violations/2andSignature.violations/2, both using:xmerl_xpathand never:xmerl_scan, so the wrapper invariant staysintact after ✨ Feature / Metadata validation — certificate and ds:Signature structure rules #31 lands.
🐛 Fix / UTF-8 characters in SAMLResponse rejected by xmerl_scan #22 (UTF-8 fix) — touched four files because the binary→charlist
conversion was duplicated. After PR 1 lands, equivalent fixes are a
one-file diff.
🔐 Security / Fix XXE, NotBefore validation, algorithm whitelist #12 (XXE + NotBefore + algorithm allow-list) — introduced the
hardened
:xmerl_scanoptions that PR 1 now centralises. The optionsthemselves were already in production before this initiative; this
work hardens the structural guarantee that they cannot be bypassed
at a new call site.
👷 CI / GitHub actions security and quality checks #18 (GitHub Actions security and quality checks) — the existing CI
runs Sobelow / Credo / Dialyzer /
mix_audit/hex.audit. Thoseare generic. PR 4 adds the SAML-domain-aware gate that complements
them.
References
🐛 Bug / Remove HTTP-Redirect AssertionConsumerService from generated SP metadata #19 (HTTP-Redirect ACS removal — adjacent hardening), 🐛 Fix / UTF-8 characters in SAMLResponse rejected by xmerl_scan #22, 🔐 Security / Fix XXE, NotBefore validation, algorithm whitelist #12.
metadata-validation umbrella), ✨ Feature / Metadata validation — certificate and ds:Signature structure rules #31 (under ✨ Feature / SAML metadata validation #17).