Skip to content

fix(processor,security): stop /health from exposing raw SIEM webhook error bodies (#989) - #1972

Merged
2witstudios merged 3 commits into
masterfrom
pu/security-health-exposes-raw-siem-webhook-error-bodies-unauthenticated
Jul 9, 2026
Merged

2witstudios merged 3 commits into
masterfrom
pu/security-health-exposes-raw-siem-webhook-error-bodies-unauthenticated

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 9, 2026 •

Copy link
Copy Markdown
Owner

Closes #989.

Problem

The processor's GET /health endpoint is unauthenticated by design (k8s liveness probe) but surfaced siem.sources.<source>.lastError, which was populated verbatim from the SIEM delivery worker as HTTP <status>: <full response.text()>. If a customer's SIEM receiver returns a verbose error (stack trace, invalid token: abc123…, schema, user IDs), that content became readable by anyone who can reach the health endpoint. In cloud mode where processors share infrastructure this is a cross-tenant information-disclosure surface; even on-prem it aids downstream-SIEM fingerprinting.

Fix — zero-trust + pure functions

The untrusted external body must never cross into the persisted/exposed domain. Enforced at three points:

  1. Classify at the write boundary (siem-adapter.ts) — a closed union DeliveryErrorClass (transport_error, http_client_error, http_server_error, ssrf_blocked, invalid_config, chain_tamper, preflight_unavailable, internal_error, unclassified_error) is stamped on every failed SiemDeliveryResult. A pure classifyHttpStatus() handles the 4xx/5xx split. The raw error string is kept for logging only.
  2. Make illegal states unrepresentable (siem-delivery-worker.ts) — recordError's signature changed from message: string to errorClass: DeliveryErrorClass. It is now a compile-time error to persist a raw body into the /health-visible cursor from any call site.
  3. Read-time zero-trust guard (siem-health-builder.ts) — cursorToPerSource allowlists lastError against the safe classes; anything else (e.g. a legacy raw-body row already at rest in the DB) collapses to unclassified_error. null is preserved so deriveStatus still reports error only when one occurred — this neutralizes pre-existing rows with no migration.

Full raw error detail is retained in the processor's stdout logs at every write site for operator triage.

Deliberately out of scope

  • No authenticated /health/detail endpoint — raw bodies already reach stdout at all write sites; a re-exposure endpoint just relocates the hazard behind an auth check that can be misconfigured.
  • No DB scrub migration — the read-time allowlist neutralizes legacy rows on the only unauthenticated surface (/siem/receipts does not read the column).

Acceptance criteria

  • siem.sources.<source>.lastError on unauthenticated /health never contains customer-controlled strings.
  • Operators can still see the raw error during triage (processor stdout logs at every write site).
  • Tests cover error-classification mapping, and that raw bodies do not appear in /health even when the cursor row contains them.
  • Change documented (this PR description + code comments referencing security: /health exposes raw SIEM webhook error bodies unauthenticated #989).

Testing

  • bun run --filter=@pagespace/processor typecheck ✓ (the recordError signature change proves at compile time no call site can persist free text)
  • bun run --filter=@pagespace/processor lint ✓
  • 221 tests pass across the four affected files, including a new end-to-end /health test proving a cursor row containing sk-live-abc123 surfaces as unclassified_error with the secret absent from the response JSON.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security Improvements

    • Sensitive SIEM delivery and webhook error details are no longer exposed through health checks or stored cursor status.
    • Error information is standardized into safe categories, including configuration, network, HTTP, validation, and tampering errors.
    • Chain verification failures now report safe status indicators without revealing forensic details.
  • Reliability

    • SIEM delivery failures retain clearer classifications while preserving retry behavior and operational diagnostics.

…error bodies (#989)

The unauthenticated processor /health endpoint surfaced
siem.sources.<source>.lastError, which was populated verbatim from the SIEM
delivery worker and could contain raw, customer-controlled webhook response
bodies (stack traces, auth tokens, PII, schema). In cloud mode this is a
cross-tenant info-disclosure surface; even on-prem it aids downstream-SIEM
fingerprinting.

Fix, following a zero-trust + pure-function shape:

- Classify at the write boundary: siem-adapter now stamps a safe, closed-union
  DeliveryErrorClass (transport_error, http_client_error, http_server_error,
  ssrf_blocked, invalid_config, chain_tamper, preflight_unavailable,
  internal_error, unclassified_error) on every failed SiemDeliveryResult. The
  raw error string is kept only for logging.
- Make illegal states unrepresentable: recordError's signature changes from
  message: string to errorClass: DeliveryErrorClass, so it is now a compile-time
  error to persist a raw body into the /health-visible cursor from any call site.
- Read-time zero-trust guard: siem-health-builder allowlists lastError against
  the safe classes; anything else (e.g. a legacy raw-body row already at rest)
  collapses to 'unclassified_error'. null is preserved so deriveStatus still
  reports 'error' only when one occurred.

Full raw error detail is retained in the processor's stdout logs at each write
site for operator triage, so no authenticated detail endpoint is added and no
data migration is required.

Tests: adapter error-class mapping (+ raw text still rides on `error` for
logging), worker persists only the safe class and never the raw body, health
builder redacts unknown values, and an end-to-end /health test proving a cursor
row containing a secret surfaces as 'unclassified_error' with the secret absent
from the response.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVkZaY1ZKwYq5K3HkgZ6A1
@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@2witstudios, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20a8e531-c31d-4be0-8776-52b8d9870aa1

📥 Commits

Reviewing files that changed from the base of the PR and between 1268b08 and 6e4766e.

📒 Files selected for processing (7)
  • apps/processor/src/__tests__/server.test.ts
  • apps/processor/src/services/__tests__/siem-adapter.test.ts
  • apps/processor/src/services/__tests__/siem-health-builder.test.ts
  • apps/processor/src/services/siem-adapter.ts
  • apps/processor/src/services/siem-health-builder.ts
  • apps/processor/src/workers/__tests__/siem-delivery-worker.test.ts
  • apps/processor/src/workers/siem-delivery-worker.ts
📝 Walkthrough

Walkthrough

SIEM delivery failures now receive safe classifications, workers persist those classes instead of raw error text, and /health redacts unrecognized cursor errors while preserving error status and recognized classifications.

Changes

Safe SIEM error handling

Layer / File(s) Summary
Delivery error classification
apps/processor/src/services/siem-adapter.ts, apps/processor/src/services/__tests__/siem-adapter.test.ts
Webhook and syslog failures now return typed classes such as ssrf_blocked, http_server_error, http_client_error, transport_error, and invalid_config; batching and retry paths propagate them.
Safe cursor error persistence
apps/processor/src/workers/siem-delivery-worker.ts, apps/processor/src/workers/__tests__/siem-delivery-worker.test.ts
Delivery, preflight, tamper, and unexpected failures persist safe classes in cursors, while raw details remain limited to console logging.
Health error normalization
apps/processor/src/services/siem-health-builder.ts, apps/processor/src/services/__tests__/siem-health-builder.test.ts, apps/processor/src/__tests__/server.test.ts
Health output passes through recognized classes and null, maps legacy raw errors to unclassified_error, and verifies secrets are absent from unauthenticated responses.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SIEMAdapter
  participant DeliveryWorker
  participant CursorDatabase
  participant HealthBuilder
  participant HealthEndpoint
  SIEMAdapter->>DeliveryWorker: return errorClass and raw error
  DeliveryWorker->>CursorDatabase: persist errorClass
  HealthEndpoint->>HealthBuilder: buildSiemHealth()
  HealthBuilder->>CursorDatabase: read cursor lastError
  HealthBuilder-->>HealthEndpoint: safe lastError classification
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR redacts raw SIEM error bodies and adds tests, but no audit entry or equivalent documentation is shown in the provided changes. Add the required audit entry or equivalent documentation describing the /health redaction and safe error-class persistence.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main security fix in the PR.
Out of Scope Changes check ✅ Passed The additional SIEM error-class hardening and tests all support the same /health redaction objective, with no clear unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/security-health-exposes-raw-siem-webhook-error-bodies-unauthenticated

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

2witstudios and others added 2 commits July 9, 2026 17:36
)

Derive both the DeliveryErrorClass union and the SAFE_DELIVERY_ERROR_CLASSES
allowlist from one `as const` array, so a new class can't be added to one
without the other. Also drop the duplicated allowlist literal in the server
test mock by spreading the real siem-adapter module via importOriginal.

No behavior change; 221 processor tests still pass, typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVkZaY1ZKwYq5K3HkgZ6A1
…sses; derive retryable from class (#989)

Proactive hardening from a self-review pass:
- siem-adapter: derive sendWebhook's `retryable` flag from classifyHttpStatus
  (5xx/429 → http_server_error → retryable) so the class and retryability can't
  drift; remove the duplicated status check.
- Tests: assert errorClass on the three previously-unasserted paths — syslog
  SSRF (ssrf_blocked), syslog validation-throw (transport_error), deliverToSiem
  misconfig (invalid_config), and the worker top-level catch (internal_error,
  never the raw message). All nine DeliveryErrorClass values are now covered.

221 processor tests pass; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVkZaY1ZKwYq5K3HkgZ6A1
@2witstudios

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@2witstudios

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 52 minutes.

@2witstudios
2witstudios merged commit 0b3f899 into master Jul 9, 2026
3 checks passed
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.

security: /health exposes raw SIEM webhook error bodies unauthenticated

1 participant