feat(integrations): Add webhook_headers field to SentryApp API#117089
Draft
billyvg wants to merge 14 commits into
Draft
feat(integrations): Add webhook_headers field to SentryApp API#117089billyvg wants to merge 14 commits into
webhook_headers field to SentryApp API#117089billyvg wants to merge 14 commits into
Conversation
d2afcd7 to
7e5a624
Compare
b78eff1 to
61e2022
Compare
webhook_headers field to SentryApp API
7e5a624 to
f8b9a90
Compare
b99a4b3 to
f034cce
Compare
billyvg
commented
Jun 10, 2026
Comment on lines
+307
to
+311
| existing_by_name = {} | ||
| for header in self.sentry_app.webhook_headers: | ||
| name, separator, _value = header.partition(":") | ||
| if separator: | ||
| existing_by_name[name.strip().lower()] = header |
Member
Author
There was a problem hiding this comment.
create a map of existing header names to the header so that we can check if the user has updated an existing header's value
| if separator and value.strip() == MASKED_VALUE: | ||
| stored = existing_by_name.get(name.strip().lower()) | ||
| if stored is not None: | ||
| resolved.append(stored) |
Member
Author
There was a problem hiding this comment.
since the read api only sends masked values, the user will be sending us a mix of masked and unmasked. assume that they are not modifying a header if its value is the masked value, also SOL if you want to change to the masked value.
78f3715 to
395266b
Compare
f034cce to
bfcfdcc
Compare
395266b to
aafdc08
Compare
fd01108 to
85b577d
Compare
Member
Author
|
@sentry review |
aafdc08 to
0903390
Compare
7820a8e to
f15bf3a
Compare
081133b to
22619f5
Compare
f15bf3a to
2a5161c
Compare
2a5161c to
5de0300
Compare
a171cda to
d134a00
Compare
5de0300 to
069446e
Compare
d134a00 to
8000015
Compare
75192ba to
2da7b51
Compare
Add a webhook_headers ArrayField to SentryApp so custom integrations can store HTTP headers (one 'Header-Name: value' per entry) to send with every outgoing webhook request. This is the storage layer for the Webhook Headers UI. The field is stored on SentryApp alongside webhook_url (not on ApiApplication where allowed_origins lives) since headers are a property of the webhook. Values may contain secrets (e.g. bearer tokens), so the relocation sanitizer scrubs webhook_headers from exports, matching how the events array is handled. Uses db_default=[] for a safe zero-downtime additive migration. Co-Authored-By: Claude <noreply@anthropic.com>
Wire webhook_headers through the SentryApp create/update API so custom integrations can manage the headers stored in the prior commit. - Parser validates each entry is a single 'Header-Name: value' pair, rejects CR/LF (header injection), and rejects reserved headers (Content-Type, Host, Request-ID, and the Sentry-Hook-* family) so they cannot be overridden. - Serializer masks header values on read, revealing real values only to viewers allowed to see secrets (same gate as clientSecret, minus its time window since headers must stay editable). - Creator and updater persist the field. The updater preserves stored values for entries resubmitted with the mask sentinel, so a form prefill+resave does not clobber real secrets; an empty list clears all headers. Co-Authored-By: Claude <noreply@anthropic.com>
…eaders Regression test confirming the serializer unmasks webhook_headers for internal integration owners, not just public apps. The unmasking lives in the owner visibility block (not the non-internal featureData block), so internal apps are covered. Co-Authored-By: Claude <noreply@anthropic.com>
The webhookHeaders response field is required, so the OpenAPI example validator (run with --no-additional-properties) failed for every documented SentryApp response example. Add webhookHeaders to the retrieve, update, and organization-list examples so the api docs test passes. Co-Authored-By: Claude <noreply@anthropic.com>
Webhook header values are masked on read, and the updater re-pairs masked entries to stored values by header name on save. Reject duplicate names (case-insensitive) in the parser so that re-pairing is always unambiguous and a prefill+resave can't mis-restore or lose a value. Also document the remaining masked round-trip limitation: renaming a header while leaving its value masked drops the entry (only reachable by an editor who sees masks, i.e. org:write without scope coverage). Co-Authored-By: Claude <noreply@anthropic.com>
…t scrubbing Lock in the security and correctness guarantees of the webhook header masking logic that were previously only described in comments: - CR/LF rejection guards against header injection / request splitting - reserved-header check is case-insensitive (relies on .lower()) - a masked entry with no stored match is dropped, never persisting the literal mask placeholder as a real header value - the documented rename-while-masked drop behavior is pinned - relocation export scrubs webhook_headers so secrets never leave Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two gaps in the custom webhook headers validator: - No upper bound on the number of headers allowed a user to configure hundreds, inflating every outgoing webhook request and the stored ArrayField without limit. Cap at 20. - Header names were only checked for CR/LF (header injection) but not for RFC 7230 token characters. urllib3 does not validate names before sending, so a control character embedded in an x-* name (e.g. X-Evil\x01Header) would be sent verbatim. Add _HTTP_TOKEN_RE to enforce the token character set after partitioning the name. Three new tests cover the count limit, a control-character name, and a space in a header name. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Narrow the custom webhook header allow list to only the Authorization header and X-* custom headers. Previously User-Agent, Accept, Date, and Prefer were also permitted; these are removed so callers can only set an auth credential and their own namespaced custom headers. The X-* allowance is unchanged (it is a separate prefix check), and the reserved-header guards still take precedence, so headers like X-Forwarded-* and X-Sentry-* remain blocked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8000015 to
d082a93
Compare
2da7b51 to
b695797
Compare
With the allow list narrowed to Authorization and X-*, the reserved checks for non-X- headers (content-type, host, request-id, and the sentry-hook prefix) are unreachable: anything that is not Authorization or X-* is already rejected by the allow list before the reserved check runs. Trim RESERVED_WEBHOOK_HEADERS and RESERVED_WEBHOOK_HEADER_PREFIXES to only the X-* names that must be carved out of the X-* allowance, and retarget the case-insensitivity test at those entries so it exercises the reserved branch. No change in which headers are accepted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This updates the SentryApp API endpoint to accept a
webhookHeadersfield, which is a newline separated string of http headers that will be sent with the integration's webhooks.Since a use-case can be attaching an Authorization header, we want to mask the header values
unless you have sufficient access (e.g. integration owner or org:write access). [i've changed this so it's always masked] I believe we have sufficient test cases testing updates when we have masked values and are adding more.You are only allowed to define the following headers:
user-agentacceptdatepreferSlop
Wire webhook_headers through the SentryApp create/update API so custom
integrations can manage the headers stored in the prior commit.
CR/LF (header injection), and rejects reserved headers (Content-Type, Host,
Request-ID, and the Sentry-Hook-* family) so they cannot be overridden.
allowed to see secrets (same gate as clientSecret, minus its time window since
headers must stay editable).
entries resubmitted with the mask sentinel, so a form prefill+resave does not
clobber real secrets; an empty list clears all headers.
Co-Authored-By: Claude noreply@anthropic.com
Stack created with GitHub Stacks CLI • Give Feedback 💬