Skip to content

UX: scope env-supplied sntrys_ tokens from embedded url claim when SENTRY_HOST is unset #848

@BYK

Description

@BYK

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:

  1. Generates an org-auth-token in their self-hosted Sentry settings UI (e.g. for https://sentry.acme.com).
  2. Exports SENTRY_AUTH_TOKEN=sntrys_... in their shell.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions