Skip to content

fix: validate ciphertext length before CBC/GCM decryption (Sentry W9)#9

Merged
pavelzeman merged 4 commits into
masterfrom
fix/cbc-decrypt-input-validation
Mar 24, 2026
Merged

fix: validate ciphertext length before CBC/GCM decryption (Sentry W9)#9
pavelzeman merged 4 commits into
masterfrom
fix/cbc-decrypt-input-validation

Conversation

@pavelzeman

@pavelzeman pavelzeman commented Mar 20, 2026

Copy link
Copy Markdown

Summary

Sentry: W9 — SAML login crash · 11 crashes · 1 affected instance (anselm-mm.devproxy.linotp.de) · first seen 2026-02-23

A malformed encrypted SAML assertion causes crypto/cipher.CBCDecrypter.CryptBlocks to panic with input not full blocks, crashing the server process. Every SAML login attempt from the affected IdP triggers the crash — users cannot log in and the server must be restarted.


Changes

1. Input validation for DecryptBytes

Add bounds checks before calling CryptBlocks/GCM.Open so malformed ciphertext returns a descriptive error instead of panicking.

When does this happen?

  • Truncated HTTP response — reverse proxy, WAF, or load balancer cuts the SAML response (AWS ALB body limits, nginx client_max_body_size, proxy timeouts)
  • Malformed IdP response — non-standard or misconfigured IdP emits incorrect CipherValue (algorithm mismatch, key size confusion, broken XML encryption)
  • Base64/encoding corruption — character re-encoding in transit, + chars URL-encoded inconsistently in SAML POST binding
  • Adversarial input — crafted SAML response probing the SP; a panic = denial of service

Validations added

Mode Check Why
CBC len(data) >= 2*blockSize Need at least IV + one encrypted block
CBC len(data) % blockSize == 0 CryptBlocks panics otherwise
CBC PKCS#7 pad byte 0 < pad <= blockSize Prevents out-of-bounds slice on garbage padding
CBC Post-trim data non-empty Catches all-zero-byte decryption results
GCM len(data) >= nonceSize Need at least the 12-byte nonce before calling Open

2. Fix expired test certificate (clock pinning)

The idpCertificate in test_constants.go expired 2026-02-09, silently breaking TestSAML. The last CI run on master was 2025-06-02 — before the expiry. No commits since = no CI runs = undetected failure.

Fix: Pin the test clock to 2025-01-01 (within the cert's validity window) and use a custom key store whose certificate is also valid at the pinned time.


⚠️ Follow-up: Certificate Renewal

The clock-pinning is a workaround. The proper fix is to regenerate the test certificate with a longer validity period.

Steps

  1. Generate a new test cert (valid 100 years):
openssl req -x509 -newkey rsa:2048 -keyout /dev/null -nodes \
  -out idp_test_cert.pem -days 36500 \
  -subj "/C=US/ST=California/L=San Francisco/O=Okta/OU=SSOProvider/CN=dev-116807/emailAddress=info@okta.com"
  1. Replace idpCertificate in test_constants.go with the new PEM.

  2. Audit tamper-detection testsmanInTheMiddledResponse and similar embed the old cert's signature. Most tests re-sign at runtime so they'll work, but tamper tests may need their embedded X509Certificate elements updated.

  3. Regenerate testdata/test.crt + testdata/test.key (also expired, May 2016) and re-encrypt testdata/saml.post:

openssl req -x509 -newkey rsa:2048 -keyout testdata/test.key -nodes \
  -out testdata/test.crt -days 36500 -subj "/CN=test"
  1. Remove the clock-pinning workaround from saml_test.go and revert to dsig.RandomKeyStoreForTest().

Complexity estimate

  • Cert generation + replacement: ~10 min
  • Audit tamper-detection tests + re-encrypt test data: 30-60 min
  • Total: ~1 hour

Release Note

Fixed a server crash when processing malformed or truncated encrypted SAML assertions during login.

Co-authored-by: Claude claude@anthropic.com

Release Note

Fixed a panic in SAML response decryption when the server receives a malformed or truncated ciphertext (Sentry W9).

pavelzeman and others added 2 commits March 20, 2026 03:25
Add input validation to DecryptBytes to prevent panics from malformed
or truncated SAML responses.

crypto/cipher.CBCDecrypter.CryptBlocks panics with 'input not full blocks'
if the ciphertext length is not a multiple of the block size. A malformed
SAML response (e.g., truncated by a reverse proxy, corrupted in transit,
or from a misconfigured IdP) can trigger this.

Validations added:
- CBC: ciphertext must be at least 2*blockSize (IV + one block minimum)
- CBC: data after IV must be a multiple of the block size
- CBC: PKCS#7 padding byte must be valid (0 < pad <= blockSize)
- CBC: decrypted data must not be empty after zero-byte trimming
- GCM: ciphertext must be at least nonceSize bytes

Sentry: https://mattermost-mr.sentry.io/issues/7286219464/ (W9, SAML login crash)

Co-authored-by: Claude <claude@anthropic.com>
The idpCertificate in test_constants.go expired 2026-02-09, causing
TestSAML to fail with 'Cert is not valid at this time' instead of
testing actual SAML validation logic.

Fix: Pin the test clock to 2025-01-01 (within the cert's validity
window) and replace dsig.RandomKeyStoreForTest() with a custom key
store whose certificate is also valid at the pinned time. The two
must overlap because the cert store contains both the idpCertificate
and the test signing key's certificate.

This is a workaround. The proper fix is to regenerate the test
certificate with a longer validity period (see PR description).

Co-authored-by: Claude <claude@anthropic.com>
@pavelzeman pavelzeman marked this pull request as ready for review March 20, 2026 14:00
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5b0eac9-2e35-460c-8d7f-4b416b739ef4

📥 Commits

Reviewing files that changed from the base of the PR and between e1cf8af and 4e4ef54.

📒 Files selected for processing (1)
  • saml_test.go

📝 Walkthrough

Walkthrough

Adds strict input and padding validation to encrypted-assertion decryption paths (AES-GCM and CBC) and new tests covering truncated/invalid ciphertexts. Updates a SAML test to use a pinned-time keystore and fake clock for deterministic certificate/signature validity checks.

Changes

Cohort / File(s) Summary
SAML Test Setup
saml_test.go
Replaces randomized test key store with a testKeyStoreAt keyed to a pinned time, configures SAMLServiceProvider.Clock with a fake clock, and generates SP keypair/certificate valid relative to the pinned timestamp for deterministic validation.
Encrypted Assertion Decryption
types/encrypted_assertion.go
Adds explicit AES-GCM ciphertext length check before nonce extraction; centralizes CBC error handling, enforces minimum ciphertext length, block-aligned length after IV, non-empty decrypted output, and strict PKCS#7 padding validation (non-zero, ≤ block size, ≤ plaintext length).
Encrypted Assertion Tests
types/encrypted_assertion_test.go
New tests and helpers: certificate/key generation, RSA-OAEP symmetric key encryption, and cases asserting errors for CBC truncation/length edge cases, GCM undersized ciphertexts, and invalid CBC padding without causing panics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding ciphertext length validation for CBC/GCM decryption to fix a Sentry crash (W9).
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the crash scenario, validation logic, certificate expiry workaround, and release notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cbc-decrypt-input-validation

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

@pavelzeman pavelzeman requested a review from esarafianou March 20, 2026 15:52
Distinct error messages for different CBC failure modes (truncated
ciphertext, misaligned blocks, invalid PKCS#7 padding) would allow
an attacker to perform a padding oracle attack by distinguishing
padding failures from other errors. Consolidate all CBC decryption
errors into a single generic message.

Made-with: Cursor

@esarafianou esarafianou left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The distinct CBC error messages ("ciphertext too short", "not a multiple of block size", "invalid PKCS#7 padding") let an attacker distinguish padding failures from other errors, enabling block-by-block decryption of the assertion (padding oracle attack). This is exploitable when the SAMLResponse element is unsigned and only the SAMLAssertion is signed, since decryption runs before assertion signature validation.

Mattermost Server strips these errors in production, so Mattermost wouldn't be vulnerable to this attack but as gosaml2 under our Github org is an open source library, it should be secure by default.

Mattermost Server doesn't log the detailed error either, so having separate error messages wouldn't provide debugging value either.

I pushed a commit to consolidate the errors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@saml_test.go`:
- Around line 178-180: The call to testKS.GetKeyPair() returns an error that is
ignored and then overwritten by the next assignment to err; after calling
GetKeyPair() (where you destructure to "_, _cert, err := testKS.GetKeyPair()")
check err immediately and handle it (e.g., t.Fatalf or return) before proceeding
to x509.ParseCertificate; ensure you do not reuse/overwrite the err without
validating it so that any failure from GetKeyPair() is surfaced in the test.

In `@types/encrypted_assertion_test.go`:
- Around line 17-29: Replace the flaky random-byte case in the test in
types/encrypted_assertion_test.go with a deterministic, known-bad PKCS#7 payload
and assert that DecryptBytes returns an error containing "failed to decrypt CBC
ciphertext"; specifically, construct a malformed PKCS#7/CMS block (invalid PKCS7
padding/CBC ciphertext) instead of crypto/rand-generated bytes, call
DecryptBytes (or the test helper that invokes it) with that payload, and change
the assertion to require the exact CBC decryption failure string so the
regression is deterministic and fails if DecryptBytes silently strips a
malformed trailer.

In `@types/encrypted_assertion.go`:
- Around line 90-104: The CBC unpadding currently trims trailing NULs and only
range-checks the final pad byte, which lets malformed PKCS#7 trailers through;
modify the logic in the unpadding code (working with variables data, padLength,
blockSize, cbcErr) to NOT call bytes.TrimRight before validation, then after
computing padLength validate that padLength > 0, padLength <= blockSize and
padLength <= len(data), and verify that the last padLength bytes all equal
byte(padLength); if any check fails return nil, cbcErr, otherwise return
data[:len(data)-padLength].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1c98147-cf45-4198-81e6-0ef319eb8cd5

📥 Commits

Reviewing files that changed from the base of the PR and between 4f517d3 and e1cf8af.

📒 Files selected for processing (3)
  • saml_test.go
  • types/encrypted_assertion.go
  • types/encrypted_assertion_test.go

Comment thread saml_test.go
Comment thread types/encrypted_assertion_test.go
Comment thread types/encrypted_assertion.go
@esarafianou

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@esarafianou

Copy link
Copy Markdown

@coderabbitai resolve

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Comments resolved and changes approved.

@pavelzeman pavelzeman merged commit 8dc2d23 into master Mar 24, 2026
7 checks passed
@pavelzeman pavelzeman deleted the fix/cbc-decrypt-input-validation branch March 24, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants