diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8ef2060 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,89 @@ +# Security Policy + +`ex_saml` is a SAML 2.0 **Service Provider (SP)** library. It consumes and +validates IdP-issued SAML responses/assertions and produces SP requests and +metadata. Because it sits directly on an authentication trust boundary, this +document states what the library defends against, where the trust boundary +sits, and how to report issues. + +## Supported versions + +| Version | Supported | +|---------|-----------| +| latest `1.x` | ✅ security fixes | +| `< 1.0` | ❌ | + +## Reporting a vulnerability + +Please report suspected vulnerabilities **privately** — do not open a public +issue for an unpatched flaw. + +- Open a [GitHub security advisory](https://github.com/docJerem/ex_saml/security/advisories/new), or +- email the maintainer (see the repository owner's profile). + +Include: affected version, a description, and a proof-of-concept if possible. +We aim to acknowledge within a few business days and to coordinate a fix and +disclosure timeline with you. + +## Threat model + +The SP consumes attacker-reachable input: an IdP `SAMLResponse` POSTed (or +redirected) to the ACS endpoint. The following classes are explicitly in +scope. + +| Threat | Defense | Where | +|--------|---------|-------| +| **XXE / external entities** (external/parameter entities, external DTD) | `:xmerl_scan` is always called with `allow_entities: false` (and `namespace_conformant: true`) | `Core.Binding`, `Core.Sp`, `Metadata` | +| **Entity-expansion DoS** (billion-laughs) | Same — entity expansion is disabled, so nested/expanding entities cannot be processed | as above | +| **Signature forgery** | Signatures are verified with `:public_key.verify/4`; a forged/incorrect signature fails | `Core.Xml.Dsig.verify/2` | +| **Weak signature algorithms** | **RSA-SHA1 is rejected** as cryptographically broken; RSA-SHA256 is accepted | `Core.Xml.Dsig` | +| **Key substitution via document `KeyInfo`** | Trust is bound to the **configured IdP certificate fingerprints**, never to a document-supplied `KeyInfo`. The certificate carried in the signature is compared against the SP config's `trusted_fingerprints` before the assertion is trusted | `Dsig.check_fingerprints/2`, `IdpData` | +| **IdP / issuer confusion** | The assertion `Issuer` is validated against the configured IdP `entity_id` when available | `Core.Saml.validate_assertion/4` | +| **Audience / recipient / destination misuse** | `Recipient` must match the SP ACS; `Audience` is checked against the SP entity id when present | `Core.Saml` | +| **Time-based replay / stale assertions** | `NotBefore` (with small skew) and `NotOnOrAfter` are enforced | `Core.Saml` | +| **RelayState / one-time consumption** | Anti-replay of the transaction is delegated to the injected state cache (consumer-provided) | consumer + `ExSaml.State` | + +### Trust boundary + +> **Signatures are only ever trusted when the signing certificate matches a +> fingerprint configured for that IdP.** The library never derives trust from +> data supplied inside the document (e.g. an inline `KeyInfo`/certificate that +> the response itself carries). This is the single most important invariant: +> attacker-supplied XML cannot introduce a key the SP will trust. + +## Known hardening in progress + +These are tracked as open work and are **not yet complete**: + +- **XML Signature Wrapping (XSW)** — making the "verified node == consumed + node" binding explicit and adding a dedicated XSW test corpus (#39). +- **Adversarial XML corpus in CI** — a versioned fixture set (XXE, DOCTYPE, + entity expansion, encoding, c14n golden outputs) run on every build (#38). +- **Parsing-surface centralisation + audit gate** — a single hardened parse + entry point and a CI check forbidding new unhardened `:xmerl_scan` sites + (#32). + +## Algorithm policy + +| Purpose | Accepted | Rejected | +|---------|----------|----------| +| Signature | RSA-SHA256 | RSA-SHA1 (broken) | +| Digest | SHA-256 | SHA-1 | + +## Non-goals + +`ex_saml` is a SAML **SP** library only. Out of scope: + +- Acting as an Identity Provider (IdP). +- OIDC / OAuth 2.0 flows. +- Session management, user storage, or authorization decisions (the consuming + application owns these). +- Transport security (TLS termination), which is the deployment's + responsibility. + +## Coordinated disclosure + +We follow coordinated disclosure. After a fix ships, a write-up is published +under [`docs/advisories/`](docs/advisories/) following +[`docs/advisories/TEMPLATE.md`](docs/advisories/TEMPLATE.md): root cause, +impact, affected versions, and the fix. diff --git a/docs/advisories/TEMPLATE.md b/docs/advisories/TEMPLATE.md new file mode 100644 index 0000000..7bf41c7 --- /dev/null +++ b/docs/advisories/TEMPLATE.md @@ -0,0 +1,41 @@ +# Advisory: + +- **Identifier:** GHSA-xxxx / CVE-xxxx (if assigned) +- **Severity:** Critical | High | Medium | Low +- **Affected versions:** `>= x.y.z, < a.b.c` +- **Fixed in:** `a.b.c` +- **Reported by:** +- **Published:** YYYY-MM-DD + +## Summary + +One or two sentences: what the flaw is and the impact in plain terms. + +## Root cause + +The technical cause — the code path, the missing/incorrect check, and why it +was exploitable. Reference the relevant module/function. + +## Impact + +What an attacker can achieve (e.g. authentication bypass, DoS, information +disclosure), and under what preconditions. + +## Proof of concept + +Minimal reproduction (redacted as needed). + +## Fix + +What changed and why it closes the issue. Link the PR/commit. + +## Mitigation / workaround + +Any interim mitigation for users who cannot upgrade immediately. + +## Timeline + +- YYYY-MM-DD — reported +- YYYY-MM-DD — fix merged +- YYYY-MM-DD — release published +- YYYY-MM-DD — advisory published diff --git a/mix.lock b/mix.lock index e2eb94c..fa65c9f 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "nebulex": {:hex, :nebulex, "2.6.6", "677e27fcfa89eaa085d9509d5e066f305f98c1b2264ce6676eaca6fb08d4939e", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.1", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "8cbf531af6fe407383b6ba410a43a19319af47804929d8a8d1975a780b9952df"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, - "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"}, + "plug": {:hex, :plug, "1.20.2", "adbee2441232412e37fbb357fd5e4cd533fdd253b29f2e1992262b0f1fb01462", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b16baf55877d60891002ffc1ce0b3ff7d6f30a38a23e02e4d4293c4ac266f136"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},