Follow-up to #844 (host-scoped tokens / credential-exfiltration fix).
Background
Sentry's org-auth tokens (sntrys_ prefix, generated by getsentry/sentry/src/sentry/utils/security/orgauthtoken_token.py) embed a JSON payload that includes the issuing host:
sntrys_<base64(JSON{iat, url, region_url, org})>_<random-secret>
The CLI currently treats all env-supplied tokens (SENTRY_AUTH_TOKEN, SENTRY_TOKEN, [auth] token from .sentryclirc) as opaque and scopes them to whatever SENTRY_HOST/SENTRY_URL is set in the boot env, defaulting to SaaS when both are unset.
UX problem
A self-hosted user who:
- Generates an org-auth-token in their self-hosted Sentry settings UI (e.g. for
https://sentry.acme.com).
- Exports
SENTRY_AUTH_TOKEN=sntrys_... in their shell.
- Forgets to also export
SENTRY_HOST=https://sentry.acme.com.
…will today get "Refusing to send credentials to https://sentry.acme.com: active token is scoped to https://sentry.io\" on every command, because the env-token-host snapshot defaulted to SaaS and the user's .sentryclirc (or URL arg) targets the actual self-hosted instance.
The token itself contains the right answer (url: https://sentry.acme.com in its base64 payload), but the CLI doesn't read it.
Proposed enhancement
When the env-token-host snapshot would otherwise default to DEFAULT_SENTRY_URL (SaaS) AND the env token is a sntrys_-prefixed format with a parseable url claim, use the claim as the snapshot value instead of SaaS default.
Concretely:
- New helper in
src/lib/token-host.ts (or a new src/lib/token-claims.ts): extractTokenUrlClaim(token: string): string | undefined that:
- Returns
undefined for non-sntrys_ tokens (other prefixes carry no claim).
- Strips the
sntrys_ prefix, takes the chunk before the last _, base64-decodes, JSON-parses.
- Validates
iat is present (matches server-side parse_token shape).
- Returns the
url field if present and parseable as a URL origin.
- Bounded: rejects tokens longer than ~2KB to prevent DoS via huge claims.
- Catches all parse errors silently — returns undefined on any failure.
captureEnvTokenHost() consults this only when the env doesn't already provide a host. Existing callers don't change.
Explicit non-goal: not a security signal
The sntrys_ claim is unsigned — the format is plaintext base64 with a random-secret tail. An attacker can craft a sntrys_<base64-of-anything>_<random> with whatever url they want. The CLI MUST NOT use the claim for any trust decision; it's purely a UX hint for legitimately-issued tokens (where the url is authoritative because the real Sentry server wrote it).
The comment in extractTokenUrlClaim's JSDoc should make this explicit so future refactors don't accidentally promote it to a security signal.
Scope of this enhancement
sntrys_ only (per server format, only org-auth tokens carry the claim).
sntryu_ user tokens (the most common CI pattern) have no embedded host and aren't helped — no change for them.
- OAuth access tokens issued by the device flow already get correct host scoping via the
auth.host column, so this enhancement doesn't apply.
Why this is not part of #844
The host-scoping fix in #844 closes 4 CVE-class credential exfiltration vulnerabilities. The token-claim path provides zero security benefit on top of that fix and is purely a UX improvement for a narrow case. Keeping the security PR focused on the security invariants (with auth.host + boot env snapshot as the trust source) avoids coupling the CLI's security properties to an undocumented internal Sentry token format.
Acceptance criteria
- A self-hosted user with
SENTRY_AUTH_TOKEN=sntrys_<...> and no SENTRY_HOST set, who runs sentry issue list <self-hosted-url>/organizations/x/, succeeds without an explicit SENTRY_HOST export.
- All existing tests in
test/lib/security/ pass — the claim is never used for trust decisions.
- New unit test:
extractTokenUrlClaim returns undefined for malformed/oversized/forged-but-non-parseable inputs.
- New regression test: even when the token's claim says `https://evil.com\`, an explicit
SENTRY_HOST=https://sentry.io in env wins (the env snapshot is authoritative; the claim is only a fallback).
Follow-up to #844 (host-scoped tokens / credential-exfiltration fix).
Background
Sentry's org-auth tokens (
sntrys_prefix, generated bygetsentry/sentry/src/sentry/utils/security/orgauthtoken_token.py) embed a JSON payload that includes the issuing host:The CLI currently treats all env-supplied tokens (
SENTRY_AUTH_TOKEN,SENTRY_TOKEN,[auth] tokenfrom.sentryclirc) as opaque and scopes them to whateverSENTRY_HOST/SENTRY_URLis set in the boot env, defaulting to SaaS when both are unset.UX problem
A self-hosted user who:
https://sentry.acme.com).SENTRY_AUTH_TOKEN=sntrys_...in their shell.SENTRY_HOST=https://sentry.acme.com.…will today get "Refusing to send credentials to https://sentry.acme.com: active token is scoped to https://sentry.io\" on every command, because the env-token-host snapshot defaulted to SaaS and the user's
.sentryclirc(or URL arg) targets the actual self-hosted instance.The token itself contains the right answer (
url: https://sentry.acme.comin its base64 payload), but the CLI doesn't read it.Proposed enhancement
When the env-token-host snapshot would otherwise default to
DEFAULT_SENTRY_URL(SaaS) AND the env token is asntrys_-prefixed format with a parseableurlclaim, use the claim as the snapshot value instead of SaaS default.Concretely:
src/lib/token-host.ts(or a newsrc/lib/token-claims.ts):extractTokenUrlClaim(token: string): string | undefinedthat:undefinedfor non-sntrys_tokens (other prefixes carry no claim).sntrys_prefix, takes the chunk before the last_, base64-decodes, JSON-parses.iatis present (matches server-sideparse_tokenshape).urlfield if present and parseable as a URL origin.captureEnvTokenHost()consults this only when the env doesn't already provide a host. Existing callers don't change.Explicit non-goal: not a security signal
The
sntrys_claim is unsigned — the format is plaintext base64 with a random-secret tail. An attacker can craft asntrys_<base64-of-anything>_<random>with whateverurlthey want. The CLI MUST NOT use the claim for any trust decision; it's purely a UX hint for legitimately-issued tokens (where theurlis authoritative because the real Sentry server wrote it).The comment in
extractTokenUrlClaim's JSDoc should make this explicit so future refactors don't accidentally promote it to a security signal.Scope of this enhancement
sntrys_only (per server format, only org-auth tokens carry the claim).sntryu_user tokens (the most common CI pattern) have no embedded host and aren't helped — no change for them.auth.hostcolumn, so this enhancement doesn't apply.Why this is not part of #844
The host-scoping fix in #844 closes 4 CVE-class credential exfiltration vulnerabilities. The token-claim path provides zero security benefit on top of that fix and is purely a UX improvement for a narrow case. Keeping the security PR focused on the security invariants (with
auth.host+ boot env snapshot as the trust source) avoids coupling the CLI's security properties to an undocumented internal Sentry token format.Acceptance criteria
SENTRY_AUTH_TOKEN=sntrys_<...>and noSENTRY_HOSTset, who runssentry issue list <self-hosted-url>/organizations/x/, succeeds without an explicitSENTRY_HOSTexport.test/lib/security/pass — the claim is never used for trust decisions.extractTokenUrlClaimreturnsundefinedfor malformed/oversized/forged-but-non-parseable inputs.SENTRY_HOST=https://sentry.ioin env wins (the env snapshot is authoritative; the claim is only a fallback).