Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 41 additions & 0 deletions docs/advisories/TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Advisory: <short title>

- **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:** <name / anonymous>
- **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
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
Loading