# rust-jwt-simple Audit Findings
Security audit of `rust-jwt-simple` at commit `3da7ffb1bd84`. Each finding links to a detailed write-up (`NNN-*.md`) and a proposed patch (`NNN-*.patch`).
## Summary
**Total findings: 9** (High: 2, Medium: 6, Low: 1)
## Findings
### Authorization bypass (4)
| # | Finding | Severity | File | Patch |
| ----------------------------------------------------------------- | --------------------------------------------------- | -------- | ------------------------ | ---------------------------------------------------------------------- |
| [005](005-jwe-decryption-skips-claim-validation-by-default.md) | JWE decryption skips claim validation by default | High | `src/jwe_token.rs` | [patch](005-jwe-decryption-skips-claim-validation-by-default.patch) |
| [009](009-cwt-custom-claim-deserialization-fails-open.md) | CWT custom-claim deserialization fails open | High | `src/cwt_token.rs` | [patch](009-cwt-custom-claim-deserialization-fails-open.patch) |
| [006](006-negative-jwt-timestamps-wrap-to-far-future.md) | Negative JWT timestamps wrap to far-future values | Medium | `src/serde_additions.rs` | [patch](006-negative-jwt-timestamps-wrap-to-far-future.patch) |
| [007](007-max-validity-policy-allows-tokens-without-issued-at.md) | Max-validity policy allows tokens without issued-at | Medium | `src/claims.rs` | [patch](007-max-validity-policy-allows-tokens-without-issued-at.patch) |
### Security control bypass or failure (2)
| # | Finding | Severity | File | Patch |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------- | -------- | -------------------------------- | ---------------------------------------------------------------------------------- |
| [008](008-cwt-unprotected-header-overwrites-protected-critical-parameters.md) | CWT unprotected headers overwrite protected critical parameters | Medium | `src/cwt_token.rs` | [patch](008-cwt-unprotected-header-overwrites-protected-critical-parameters.patch) |
| [001](001-rsa-modulus-minimum-check-rounds-bit-length-upward.md) | RSA modulus minimum check rounds bit length upward | Low | `src/algorithms/jwe/rsa_oaep.rs` | [patch](001-rsa-modulus-minimum-check-rounds-bit-length-upward.patch) |
### Denial of service (3)
| # | Finding | Severity | File | Patch |
| --------------------------------------------------------------------- | --------------------------------------------------------- | -------- | ------------------ | -------------------------------------------------------------------------- |
| [002](002-unbounded-cbor-materialization-in-cwt-metadata-decoding.md) | Unbounded CBOR materialization in CWT decoding | Medium | `src/cwt_token.rs` | [patch](002-unbounded-cbor-materialization-in-cwt-metadata-decoding.patch) |
| [003](003-unbounded-jwe-segment-collection.md) | Unbounded JWE segment collection before format validation | Medium | `src/jwe_token.rs` | [patch](003-unbounded-jwe-segment-collection.patch) |
| [004](004-jwe-decryption-has-no-default-token-size-limit.md) | JWE decryption has no default token size limit | Medium | `src/jwe_token.rs` | [patch](004-jwe-decryption-has-no-default-token-size-limit.patch) |
## Run details
- run id: `c2e9d7d1`
- commit: `3da7ffb1bd84f675b2c957d260f39f86731d368f`
- branch: `master`
- scope: 22 file(s), focus: `src`
## Review pass
Every finding was re-checked against the source, and every patch was applied, compiled, and exercised on both the default BoringSSL backend and the `pure-rust` backend.
All nine underlying bugs reproduce as described.
Four of the patches did not hold up.
- **001** revised. The original patch called `n().num_bits()`, which compiles on neither backend: `boring` returns `i32` where the minimum is a `u32` constant, and `superboring` has no `num_bits` method at all. The replacement measures the modulus from its big-endian bytes, which both backends expose. Severity lowered from High to Low, since 2041-bit RSA is not materially weaker than 2048-bit RSA; the real cost is that the policy check does not enforce what it claims.
- **002** revised twice. The original patch bounded the whole token by `MAX_CWT_HEADER_LENGTH` (4096 bytes), which is a header-size constant, not a token-size one; a well-formed 10 KB token would have failed metadata decoding. The second attempt used `DEFAULT_MAX_TOKEN_LENGTH` instead, which was still wrong in kind: CWT is binary and multi-gigabyte tokens are legitimate, so a one-million-byte cap on an entry point that takes no options breaks those applications with no way to opt out, while leaving `verify` exposed for exactly the applications that must set `max_token_length: None`. The fix now pre-scans the COSE framing over the raw bytes, before any CBOR is materialized, and applies the existing 4096-byte header bound up front. Payload size stays unbounded and both entry points are covered.
- **006** revised. The original patch used `u64::try_from` without importing `TryFrom`. The crate is edition 2018, where that trait is not in the prelude, so it did not compile.
- **008** revised. The original patch had a malformed hunk header and changed only `CWTToken::verify`, even though its own fix requirement calls for the same check in metadata decoding. The check now lives in a shared helper used by both paths.
- **003**, **004**, **005**, **007**, and **009** were confirmed correct in substance. The patch files for 005 and 007 had malformed hunk headers and would not apply, so every patch file has been regenerated from the tested source.
All nine patches now apply cleanly in numeric order, `cargo fmt --check` is clean, `cargo clippy` reports no new warnings, and the test suite passes on both backends across every feature combination.
### Compatibility
Nothing public is removed or changed in shape.
`DecryptionOptions` keeps its fields and still implements `Clone`, `Debug`, and `Default`; exhaustive struct literals and `..Default::default()` both still compile.
Finding 004 adds one public constant, `jwe_token::MAX_JWE_ENCRYPTED_KEY_LENGTH`.
Eight intentional behavior changes, each of which is the point of its finding:
| Finding | Input that used to be accepted and is now rejected |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 001 | RSA-OAEP keys with a 2041 to 2047-bit modulus |
| 002 | CWTs whose COSE framing is malformed or larger than 4096 bytes, rejected before decoding rather than after; also CBOR nested deeper than 16 levels anywhere in the token |
| 004 | JWE tokens over one million bytes under default options; also wrapped keys over 2048 bytes, and IVs or authentication tags of the wrong size, now rejected before the ciphertext is decoded rather than after |
| 005 | JWE tokens whose claims are expired or not yet valid |
| 006 | JWT timestamps encoded as negative JSON integers |
| 007 | Tokens without `iat` when `max_validity` is configured |
| 008 | CWTs whose protected and unprotected headers share a label |
| 009 | CWTs whose custom claims do not match the caller's claim type |
Findings 005 and 009 are the ones most likely to surface in existing deployments, since they turn silent acceptance into an error.
## How to read this directory
Each finding has a paired `NNN-<slug>.md` (the human report) and `NNN-<slug>.patch` (the proposed fix as a unified diff). The report carries the narrative; the patch is the suggested change.