Skip to content

harden(api): escape HTML in email alerts + tighten events-endpoint authz#10

Open
mikeumus wants to merge 1 commit into
mainfrom
harden/alerting-escaping-and-events-authz
Open

harden(api): escape HTML in email alerts + tighten events-endpoint authz#10
mikeumus wants to merge 1 commit into
mainfrom
harden/alerting-escaping-and-events-authz

Conversation

@mikeumus

Copy link
Copy Markdown
Contributor

Closes the two items flagged during the agent-guard security pass.

1. HTML escaping in email alerts

The finding got more serious on investigation. The email channel renders alert details into an HTML body, and accountName traces back to req.body.name at cloud-account creation — user-controlled. The cloud-monitoring alert path embeds that name in both summary and accountName. So the unescaped interpolation was reachable — not via agent-guard's fields (as first thought), but via the core cloud path.

  • Adds escapeHtml() applied to every string interpolation in the email HTML body (summary, provider, accountName, violation serviceName/metricName/values, actionsTaken).
  • Audited all channels: Slack/Discord/PagerDuty/webhook transmit via JSON.stringify (values render as text, not markup) — only the email sink needed escaping. Slack/Discord markdown from a crafted string is cosmetic, not a security issue.
  • +1 regression test: a <script> payload in accountName/serviceName/summary is neutralized to &lt;script&gt;.

2. Events endpoint authorization

POST /agent-guard/events now requires kill_switch:trigger (owner/admin/member) instead of cloud_accounts:read — an agent block is a kill-switch action, so reporting one is a write that viewers shouldn't make. GET stays cloud_accounts:read (it's a read). Role enforcement is covered by the existing permissions.test.ts matrix.

Verification

Full API suite: 845 passed / 12 skipped (+1 new). tsc clean.

🤖 Generated with Claude Code

Closes the two flagged-for-later items.

1. HTML escaping (alerting.ts): the email channel renders details into an HTML
   body, and some fields are user-controlled — `accountName` traces to
   req.body.name at cloud-account creation, and the cloud-monitoring path's
   `summary` embeds it. So the unescaped interpolation WAS reachable (via the
   core cloud path, not agent-guard). Adds escapeHtml() applied to every string
   interpolation in the email HTML (summary, provider, accountName, violation
   serviceName/metricName/values, actionsTaken). Audited the other channels:
   Slack/Discord/PagerDuty/webhook all transmit via JSON (values render as
   text), so only the email sink needed it. +1 regression test asserting a
   <script> payload is neutralized to &lt;script&gt;.

2. Events authz (agent-guard route): POST /agent-guard/events now requires
   kill_switch:trigger (owner/admin/member) instead of cloud_accounts:read —
   an agent block IS a kill-switch action, so reporting one is a write that
   viewers shouldn't make. GET stays cloud_accounts:read (it's a read).

Full API suite: 845 passed / 12 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

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.

1 participant