Skip to content

fix: Keep webhook event data as string-keyed map#108

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/sec-1230-webhook-atom-table-exhaustion
Open

fix: Keep webhook event data as string-keyed map#108
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/sec-1230-webhook-atom-table-exhaustion

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Fixes SEC-1230. Automated first-pass security fix — requires human security review before merge.

WorkOS.Webhooks.Event.new/1 recursively interned every JSON object key in a webhook payload as an atom via String.to_atom/1 (lib/workos/webhooks/event.ex:27). Atoms are never garbage-collected and the BEAM atom table is capped (default 1,048,576); exceeding it aborts the entire node, not just the request.

Webhook payload content embeds third-party data — most notably SCIM raw_attributes, whose key names are chosen by whoever controls a connected directory. A stream of genuine, correctly-signed dsync.user.* events carrying thousands of unique attribute names could therefore exhaust the atom table and crash any application using the SDK's documented construct_event/3 flow (a remote, cross-tenant denial of service). The HMAC check does not help — the events are authentic; only the key names are attacker-influenced.

The fix keeps data as a plain string-keyed map, matching the sibling WorkOS.Events.Event.cast/1, so untrusted keys are never interned:

# before: recursive convert_map/1 -> Map.put(acc, String.to_atom(key), ...)
# after:
def new(payload) do
  struct(%__MODULE__{}, %{
    id: payload["id"],
    event: payload["event"],
    data: payload["data"]   # string-keyed, no atom interning
  })
end

Breaking change — needs reviewer decision

This changes the shape of WorkOS.Webhooks.Event.data: nested keys are now strings instead of atoms (e.g. event.data["raw_attributes"]["userName"] instead of event.data.raw_attributes.userName). This is the same shape the Events API already returns, but it is a breaking change for existing webhook consumers and likely warrants a major version bump / changelog entry. An alternative (String.to_existing_atom/1 with a string fallback) would preserve atom keys for already-interned names, but produces a non-deterministic mixed atom/string map, so this PR takes the consistent string-keyed approach the finding recommends. Reviewer should confirm the preferred compatibility path.

Verification

  • mix format clean; the existing webhooks specs (updated to assert the string-keyed shape) plus a new regression test all pass.
  • New regression guard does not intern payload keys as atoms processes 25 events × 1,000 unique keys and asserts :erlang.system_info(:atom_count) stays flat.
  • Reproduced the original crash on the pre-fix code (OTP booted with a lowered atom limit +t 32768): after 3 crafted events the VM aborted with no more index entries in atom_tab (max=32768). With this fix the same sequence interns 0 new atoms and survives 1,000 events.

Link to Devin session: https://app.devin.ai/sessions/fb86c5d749264c5fbfa97cbbeb5aa86f

WorkOS.Webhooks.Event.new/1 recursively converted every JSON object key in the webhook payload into an atom via String.to_atom/1. Payload content embeds third-party data (e.g. SCIM raw_attributes whose key names are chosen by whoever controls a connected directory), so a stream of genuine, correctly-signed directory-sync events carrying many unique attribute names could exhaust the BEAM atom table (default 1,048,576) and abort the entire node.

Keep data as a string-keyed map, matching WorkOS.Events.Event.cast/1, so untrusted payload keys are never interned as atoms.
@devin-ai-integration
devin-ai-integration Bot requested a review from a team as a code owner July 22, 2026 02:50
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from Linear User

Please work on ticket "Webhooks: unbounded String.to_atom on attacker-influenced webhook payload keys enables remote BEAM atom-table exhaustion and full VM crash" (SEC-1230)

@playbook:playbook-b588614117c7477a9b9729928385384f

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@linear-code

linear-code Bot commented Jul 22, 2026

Copy link
Copy Markdown

SEC-1230

@devin-ai-integration devin-ai-integration Bot changed the title Keep webhook event data as string-keyed map fix: Keep webhook event data as string-keyed map Jul 22, 2026
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR keeps webhook event data as string-keyed JSON maps. The main changes are:

  • Removes recursive conversion of webhook data keys to atoms.
  • Aligns webhook data with the Events API representation.
  • Updates data-shape assertions to use string keys.
  • Adds coverage for atom-table exhaustion.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • The new representation matches decoded JSON and the sibling Events API.
  • The intentional data-shape change is covered by updated tests.

Important Files Changed

Filename Overview
lib/workos/webhooks/event.ex Stores decoded webhook data without interning its string keys as permanent atoms.
test/workos/webhooks_test.exs Updates expected data keys and adds coverage for processing many unique webhook keys.

Reviews (1): Last reviewed commit: "Keep webhook event data as string-keyed ..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

0 participants