Draft: Add Slack token validity check
What does this MR do and why?
This adds a validity check for Slack access tokens: bot (xoxb-), user (xoxp-), workspace access (xoxa-) and workspace (xoxs-).
A user can then tell whether the exposed credential is still live rather than only that it was leaked.
Implementation
This follows ADR-006 and uses the registry directly to map the token straight to the verifier class/client, i.e. SlackClient.
The verifier calls POST https://slack.com/api/auth.test with the token as a Bearer credential.
We map the responses to the following statuses/outcomes:
| Response | Outcome |
|---|---|
200 |
classified from the response body, see below |
429 |
RateLimitError |
500, 502, 503, 504 |
NetworkError |
| anything else | unknown |
Please note that no status code maps to inactive for this vendor – inactive comes only from the body errors listed below.
Reporting a live token as dead is worse than reporting nothing, so unmapped statuses are always unknown.
Why the status code is not enough
Slack signals the outcome in the response body rather than the status code – its
auth.test reference documents an invalid
token as {"ok": false, "error": "invalid_auth"} and instructs that "callers should always check the
value of the ok parameter in the response". A generic "200 means active" check would therefore
report every dead Slack token as active.
The verifier reads body["ok"], and classifies inactive only on the errors Slack documents as
meaning the token is dead: invalid_auth, account_inactive, token_revoked, token_expired.
ratelimited raises RateLimitError; any other error – a scope problem, for instance – is
unknown.
not_authed is deliberately not treated as dead. It means no token was sent, so it points at a
bug on our side rather than a dead credential – and reporting a token dead when we never actually
sent it is the worst outcome available.
A known coverage gap
The Slack token rule detects five kinds of Slack token: xoxb-, xoxp-, xoxa-, xoxs- and
xoxr-. This check covers the first four, which are all access tokens.
xoxr- is left out on purpose. It is a refresh token – it exists to be exchanged for an access
token and cannot authenticate an API call at all. Sending one to auth.test gets it rejected, and
that rejection is indistinguishable from a genuinely revoked token, so we would report a live
credential as dead.
When a finding is a xoxr- token, the verifier returns unknown and makes no API call. Nothing is
reported incorrectly; refresh tokens simply do not get a validity result. Covering them needs a
different check than auth.test, which is follow-up work rather than part of this MR.
Why the format check is not a copy of the rule
Everywhere else in this stack the verifier's pattern mirrors its detection rule exactly. Slack is the exception, because the rule's regex is written to spot a token in a file rather than to describe a whole one:
regex = "xox[baprs]-([0-9a-zA-Z]{10,48})"No hyphens are allowed after the prefix, so the match stops at the first one – enough for detection, but it covers only the leading section of a real token. A format check has to answer a different question ("is this entire value a token?"), which means anchoring, and anchored that regex matches none of the rule's own five examples. Mirroring it would silently disable the check.
So the pattern here encodes the real three-section shape instead, verified against the rule's
fixtures: it accepts all five examples and rejects the negativeExample with an empty secret. The
other negativeExample is placeholder filler (x repeated), which no shape-based check can exclude.
The rule's regex is worth fixing upstream so it matches its own examples; this MR does not wait on
that.
Stack Position
We have 8 MRs for all new validity checks generated during Sec Challenge #6, with the base targeting master.
Each MR in the stack adds exactly one validity check: the client class, its spec, the registry entry, and the rate limit rule.
Stack (in review order)
| # | Check | Token type |
|---|---|---|
| 1 | GitHub PAT | Github Personal Access Token |
| 2 | OpenAI project key | OpenAiProjectKey |
| 3 | Anthropic API key | anthropic_key |
| 4 | Slack access tokens | Slack token |
| 5 | Stripe live secret key | StripeLiveSecretKey |
| 6 | Datadog API key | DataDogAPIKey |
| 7 | SendGrid API token | Sendgrid API token |
| 8 | Heroku API key | Heroku API Key |
Notes
- Token type and pattern were verified against the authoritative rule in
secret-detection-rules. - The verifier's format check is narrower than the rule, on purpose – see the two sections above.
- Rate limit: 100 checks per minute per project. Slack says
auth.testallows hundreds of requests per minute. - Unmapped statuses are covered by shared example
a partner token client, which is added once and every verifier reuses. - Verifier code originates from the Sec Challenge #6 PoC branch
ghavenga-summit-ch6-validity-checks.
How to set up and validate locally
-
Run the specs:
bundle exec rspec ee/spec/lib/security/secret_detection/partner_tokens/slack_client_spec.rb \ ee/spec/services/security/secret_detection/partner_tokens/registry_spec.rb -
Confirm the registry resolves the token type and that the rate limit rule exists:
Security::SecretDetection::PartnerTokens::Registry.client_for('Slack token') # => #<Security::SecretDetection::PartnerTokens::SlackClient> Gitlab::ApplicationRateLimiter.period_for(:partner_slack_api) # => 60 seconds -
Optional, with a real credential – a revoked token should come back
inactiveand a live oneactive:Security::SecretDetection::PartnerTokens::Registry .client_for('Slack token').verify_token(ENV['TOKEN']).status
MR acceptance checklist
I have evaluated this MR against the MR acceptance checklist.