All vulnerabilities
GHSA-wfrw-rm3f-52x8

openpanel: `notification.rules` returns integration objects with plaintext Slack bot tokens / webhook URLs / auth headers to read-level project members, bypassing the redact-on-read invariant that `integration.get` and `integration.list` enforce

openpanel
CVSS
8.5High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published11 Oct 2026Updated 11 Oct 2026GitHub

What fixes it

No source in this snapshot names a fixed version.

Description

Summary

Openpanel stores integration secrets (Slack access_token, incoming_webhook.url, and webhook auth headers) in plaintext and relies on a redact-on-read invariant: integration.get and integration.list both call redactIntegration before returning. The notification.rules query does not. It is a protectedProcedure gated by enforceAccess at only the read level (bare project membership), yet it eager-loads integrations and spreads them raw into the response — so any read-level member receives the plaintext credentials of every integration attached to the project's notification rules, even though creating/editing integrations requires a higher (write/admin) privilege.

Details

packages/trpc/src/routers/notification.ts — rules:

// protectedProcedure; enforceAccess requires only level:'read' for the projectId input
const rules = await db.notificationRule.findMany({
  where: { projectId },
  include: { integrations: true },            // :52  eager-loads integration rows (with secrets)
});
return rules.map(rule => ({
  ...rule,
  integrations: rule.integrations,            // :71  spread RAW — no redactIntegration()
}));

packages/integrations/src/registry.ts: the Slack access_token, incoming_webhook.url, and webhook headers are declared as secret fields **without encrypted: true** (~:119-131,161), so they are stored plaintext; only redactConfigSecrets blanks them, and only on the read paths that call it. Contrast: integration.get (integration.ts:197) and integration.list (:223) both call redactIntegration; notification.rules and integration.delete do not.

enforceAccess (packages/trpc/src/trpc.ts:99-110) auto-derives the required level from the input; for rules the top-level projectId maps to read. Creating/updating integrations goes through upsertIntegration + assertIntegrationAccess (write/admin), so a read-level member cannot set these secrets — but can read them here. That is the privilege boundary being crossed.

Impact

Any read-level member of a project (e.g. a low-trust analyst invited to view dashboards) can call notification.rules and obtain the plaintext Slack bot token, webhook URLs, and webhook authorization headers configured by an admin. Those are live credentials to the org's external systems (post to their Slack, hit authenticated webhooks), usable outside Openpanel. Secondary: integration.delete (integration.ts:354) also returns the unredacted row, though it is write/admin-gated (lower severity).

Proof of concept (benign)

  1. As a project admin, add a Slack (or webhook) integration with a marker credential (e.g. webhook header

Authorization: Bearer hackchang-poc-marker) and attach it to a notification rule.

  1. Invite a user as a read-level member of that project (a role that cannot create/edit integrations —

verify integration.upsert is denied for them).

  1. As that read-level member, call the notification.rules tRPC query for the project and inspect

integrations[] — the marker credential is returned in plaintext (it should be redacted, as integration.get/list do). (Use marker credentials you control.)

Remediation

Apply redactIntegration to every integration object returned by notification.rules (and to the row returned by integration.delete), matching integration.get/list. Better, encrypt the secret fields at rest (encrypted: true) so a missed redact does not expose plaintext, and gate integration reads on the same privilege as integration writes.

Novelty / dedup

No published openpanel advisory covers this credential leak. Distinct from GHSA-f9rx-pxgw-c6rg (missing write-gate on report/dashboard mutations). Novel.

Disclosure

Reported privately via GitHub Private Vulnerability Reporting by @hackchang. Please coordinate a fix under a 90-day window, and publish + request a CVE when the fix ships; happy to re-test.

Text from GitHub Advisory Database

Affected versions 1

  • openpanel, openpanel-dev<= 35ddc8ebbcd4ed0767933de61f014f153d68442aGitHub