Skip to content

fix: security hardening of tokens, invites, secrets, egress and sign-in (batch 2026-10-16) - #572

Merged
fclairamb merged 115 commits into
mainfrom
security/scan-specs-2026-10-16
Oct 16, 2026
Merged

fclairamb merged 115 commits into
mainfrom
security/scan-specs-2026-10-16

Conversation

@fclairamb

@fclairamb fclairamb commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Implements the 15 specs from the 2026-10-11 → 2026-10-16 security scans, plus 3 earlier specs (OVH SMS receipts, worker env vars, LDAP docs). Each spec was implemented, audited against its text, then archived in specs/done/2026/10/.

Security fixes

  • Critical: sign-in tokens are now signed with keys specific to their purpose, so the passkey login session and the 2FA temp token are no longer accepted as access tokens.
  • Critical: accepting an invitation for an existing account now requires being signed in as that account (401 INVITE_LOGIN_REQUIRED, 403 INVITE_EMAIL_MISMATCH).
  • Critical: Kubernetes connections refuse exec / auth-provider kubeconfigs and file references. inCluster is gated by SP_K8S_ALLOW_IN_CLUSTER.
  • Critical: ${env:} references only resolve names listed in SP_CHECK_ENV_ALLOWLIST. The list is empty by default and is enforced both when a check is saved and when it runs.
  • Critical: the MySQL check builds its connection from a struct, with AllowAllFiles=false, and its dial goes through the egress guard.
  • High: Microsoft sign-in no longer links or creates an account from an unverified email ("nOAuth"). On a multi-tenant app, the email is trusted only when the ID token carries xms_edov.
  • High: status-page subscriber webhooks and publication webhooks now go through the egress guard.
  • High: updating or deleting a status-page component is scoped to its section, so another org's components can no longer be changed.
  • High: a JS docker sub-check now obeys the SaaS docker gate and the egress guard.
  • High: LAN, container and Kubernetes discovery now follow the egress policy. Refused types are hidden.
  • High: Slack inbound endpoints refuse every request when no signing secret is set, response_url must be on hooks.slack.com, and request bodies are capped at 1 MiB.
  • High: MCP OAuth tokens and scoped PATs are refused on REST, the realtime WS and the OAuth consent screen.
  • Low: the TOTP code is now asked on every SSO / OAuth sign-in.
  • Low: the 2FA, password-reset and change-password attempt counters are atomic.
  • Low: a revoked or expired PAT is evicted from the cache on every replica.

Breaking changes and operator actions

  • Every stored ${env:NAME} stops resolving until the name is listed in SP_CHECK_ENV_ALLOWLIST, on the server and on each private agent.
  • An HTTP-mode Slack install needs SP_SLACK_SIGNING_SECRET. Socket Mode does not.
  • A multi-tenant Microsoft app registration must add the email and xms_edov optional ID-token claims. The review query for existing links is in wiki/runbooks/operations-gotchas.md.
  • Kubernetes connections that use exec, auth-provider or file references stop working. Using inCluster needs SP_K8S_ALLOW_IN_CLUSTER=true.
  • Under an enforcing egress policy (SaaS by default), lan discovery is refused, and so is container discovery on SaaS shared nodes.
  • Subscriber webhooks now follow SP_EGRESS_ALLOW_PRIVATE, so self-hosted instances accept private/LAN endpoints.
  • MySQL usernames, passwords and database names can no longer contain @ / ( ) ? & :; a reference can be used instead.
  • An existing account accepting an invite must sign in first. A TOTP user signing in through SSO gets the code prompt, and the server and dashboard must ship together for that.
  • MCP OAuth tokens and scoped PATs get 401 on REST.
  • SP_SERVER_CHECK_WORKER_NB and SP_SERVER_JOB_WORKER_NB now take effect.

Batch gate

  • make build-backend lint-back test: 251/0/0 packages, 0 lint issues
  • make test-postgres: 251/0/0 packages
  • dash0 typecheck:e2e and lint:e2e: pass
  • Full Playwright run: 939 passed, 1 failed, 15 skipped. The failure, in discovery.spec.ts, was a test expectation made stale by the discovery fallback, and it is fixed in e7ce071. Only discovery.spec.ts was re-run (30/0/0), not the full suite.

Follow-ups are filed in specs/backlog/2026-10-16-16 to -21.

🤖 Generated with Claude Code

Fifteen fix specs for every critical and high finding of the first six
daily security scans, plus three lower ones (federated sign-in skips
TOTP, non-atomic attempt counters, revoked PAT cache).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.13603% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.23%. Comparing base (247e388) to head (d16e1b1).

Files with missing lines Patch % Lines
server/internal/db/postgres/postgres.go 85.71% 9 Missing ⚠️
server/internal/app/ovh_dlr_callback.go 77.14% 8 Missing ⚠️
server/internal/db/sqlite/sqlite.go 86.66% 8 Missing ⚠️
server/internal/handlers/statuspages/service.go 72.00% 7 Missing ⚠️
server/internal/handlers/auth/pat_cache.go 90.74% 5 Missing ⚠️
server/internal/handlers/checks/handler.go 16.66% 5 Missing ⚠️
server/internal/db/models/counter.go 50.00% 4 Missing ⚠️
server/internal/handlers/auth/jwt_keys.go 81.81% 4 Missing ⚠️
server/internal/handlers/auth/join_policy.go 94.11% 3 Missing ⚠️
server/internal/app/server.go 87.50% 2 Missing ⚠️
... and 15 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #572      +/-   ##
==========================================
+ Coverage   83.07%   83.23%   +0.15%     
==========================================
  Files        1200     1207       +7     
  Lines      123542   124137     +595     
==========================================
+ Hits       102632   103324     +692     
+ Misses      20902    20804      -98     
- Partials        8        9       +1     
Files with missing lines Coverage Δ
server/internal/app/aichecks.go 80.95% <100.00%> (+0.95%) ⬆️
server/internal/authhandoff/authhandoff.go 82.02% <100.00%> (+1.53%) ⬆️
server/internal/checkers/checkmysql/checker.go 53.39% <100.00%> (+6.08%) ⬆️
...rver/internal/checkers/checkmysql/config/config.go 73.23% <100.00%> (+1.81%) ⬆️
server/internal/checkers/checkmysql/liblog.go 66.66% <ø> (ø)
server/internal/checkworker/backend/ws_tunnel.go 78.00% <100.00%> (ø)
server/internal/checkworker/worker.go 90.46% <100.00%> (-0.19%) ⬇️
server/internal/config/config.go 93.54% <100.00%> (+0.20%) ⬆️
server/internal/config/discord_oauth.go 100.00% <ø> (ø)
server/internal/config/envvars.go 100.00% <100.00%> (ø)
... and 86 more

... and 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

fclairamb and others added 28 commits October 16, 2026 09:28
…settings from drifting out of the docs again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reate an account from an unverified email ("nOAuth")

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to an operator allowlist, at save time and at resolve time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m a struct, not an interpolated DSN string

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…configs and restrict `inCluster` for Kubernetes connections

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing account logs in as that account, with the invite token as the only proof

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and publication webhooks through the egress guard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Kubernetes) under the egress policy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… without a signing secret, allow-list `response_url`, cap the body

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ork on the whole REST API and the consent screen, and can mint non-expiring PATs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t and change-password attempt counters atomic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sign-in, through the existing 2FA temp-token step

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The callBack URL was registered in NewServer with the pre-overlay JWT secret
and base URL, so it never matched the token the handler checks. Register it
at the end of InitializeSystemConfig, never derive a token from the public
default JWT secret, skip registration on a localhost base URL, and log one
startup line without the token.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…red with OVH must carry the token and base URL the handler accepts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…check fetch-ahead env vars

They were ignored before (koanf env mangling and a string-blind ApplyFunc) and now apply.
Adds a docs drift test over the configuration pages and the runners wiki, a systemconfig guard that every overlay-only env var takes effect, and corrects the SP_POSTHOG_HOST default.

Changelog: SP_SERVER_CHECK_WORKER_NB and SP_SERVER_JOB_WORKER_NB were ignored before and now apply. Someone who set SP_SERVER_CHECK_WORKER_NB=3 from the old docs drops from 25 to 3 runners (the fast-lane reservation is then clamped and warns).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…onment, their documented defaults match the code, and a test keeps them matched

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nment, their documented defaults match the code, and a test keeps them matched

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an LDAP / Active Directory section (precedence, env table, TLS, filter
rules, lldap and AD examples, troubleshooting), the provider switches and
session lifetime variables, and a systemconfig test that every auth.* env var
(OIDC and SAML pending their own specs) is named in authentication.md.
LDAP failures now log their cause at debug level.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tings from drifting out of the docs again

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tings from drifting out of the docs again

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ings from drifting out of the docs again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an unverified email

On a multi-tenant registration (common / organizations / consumers), the
Graph `mail` attribute is set by any tenant admin without proof of
ownership ("nOAuth"). It was used to link the Microsoft identity to an
existing local account, or to create one (consuming invitations and
auto-join rules). The connector now reads the ID token returned by the
token endpoint (aud, exp and oid bound to the Graph id) and only trusts
the email when it is pinned to one tenant, or when the ID token carries
`email` with `xms_edov: true`. Already-linked Microsoft IDs keep signing
in.

Changelog (security, behaviour change): multi-tenant Microsoft sign-in
refuses first sign-ins with EMAIL_NOT_VERIFIED until the app registration
adds the `email` and `xms_edov` optional claims to the ID token. A pinned
SP_MICROSOFT_TENANT_ID needs nothing. Existing links are not removed;
see wiki/runbooks/operations-gotchas.md for a read-only review query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fclairamb and others added 20 commits October 16, 2026 12:25
…ounters atomic

bumpCounter read the counter, compared it in Go and wrote count+1 in three
separate statements, so a burst of parallel requests spent one unit of the
attempt budget instead of N (about 100 TOTP guesses per 15 min per IP instead
of 5, and a multiplied per-user reset-email cap). A burst on a fresh key also
inserted N duplicate global rows, since unique (organization_uid, key) never
fires for a NULL organization.

New db.Service.BumpGlobalCounter does the read, the limit check and the
increment in one transaction, serialized per key: on Postgres with
pg_advisory_xact_lock(ClassGlobalCounter, hashtext(key)) (two-int4 class
0x50020001, registered in dblock and wiki/conventions/database.md), on SQLite
by its single connection. It takes the largest live count across rows, so
duplicates left by older code are tolerated and rewritten. Keys, limits and
the sliding TTL are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…end one unit each

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e SQLite file

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd change-password attempt counters atomic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d change-password attempt counters atomic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Federated callbacks (Google, GitHub, GitLab, Microsoft, Discord, Slack,
OIDC, SAML and the Slack app install) all end in CompleteOrgLogin, which
minted a full session without looking at user.TOTPEnabled. A user with
TOTP enabled now gets no session there: the result carries requires2Fa
and a 2FA temp token naming the org and role the session would have had,
and the existing /2fa/verify and /2fa/recovery endpoints mint it once the
code is accepted (audit method "<connector>+totp").

The handoff code carries the challenge instead of tokens (exactly one of
accessToken / tempToken is accepted by Issue and Redeem), the exchange
answers requires2Fa + tempToken with no user or organization info, and no
session cookie is set until the code passes. Passkeys remain the only
sign-in without the TOTP prompt.

Changelog: Security: users with two-factor authentication enabled are now
asked for their code on SSO and OAuth sign-ins too, not only on password
and LDAP logins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prompt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d sign-in

The handoff exchange of a TOTP user now answers requires2Fa and a temp
token instead of a session. /auth/complete keeps returnTo and
membershipPending, shows the code step, and lands through
resolveHandoffLanding once verify2FA / useRecoveryCode succeed. The
TOTP / recovery form is extracted into the shared TwoFactorChallenge
component, used by the login page too and listed in the design reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also keeps /auth/complete from reading the stripped address bar as
"opened without a code" while the code step is on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge responses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gn-in, through the existing 2FA temp-token step

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n-in, through the existing 2FA temp-token step

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in the cache

The PAT validation cache now:
- caps each entry at the token's own expiry, so an expired PAT is refused
  at once instead of up to 15 minutes later;
- evicts after the row is deleted, behind a generation counter, so a lookup
  racing a revocation cannot re-cache the token;
- broadcasts every revocation on the event notifier ("auth.patrevoked",
  Postgres LISTEN/NOTIFY across replicas), and clears itself when the
  notifier reconnects;
- keeps entries 5 minutes instead of 15 (backstop for a dropped notification);
- hands each cache hit its own copy of the claims.

Release note: in a multi-replica install, revoking a personal access token
now takes effect on every replica within moments, and an expiring token stops
working at its expiry time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and reconnects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from the in-memory cache for up to 15 minutes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom the in-memory cache for up to 15 minutes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gate)

The new-scan form now falls back to an offered method when the requested
one (kubernetes, no cluster connection in the test org) is hidden, so the
kubernetes cluster picker no longer renders. The test asserts the preserved
URL param and the fallback instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…batch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fclairamb fclairamb changed the title chore(specs): security scan findings fix: security hardening of tokens, invites, secrets, egress and sign-in (batch 2026-10-16) Oct 16, 2026
Comment thread server/internal/integrations/slack/response_url.go Fixed
Comment thread server/internal/middleware/auth.go Dismissed
fclairamb and others added 2 commits October 16, 2026 13:45
…om host

Only the path and query come from the caller-supplied value, so the
request target can never be anything but Slack (CodeQL go/request-forgery).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fclairamb
fclairamb enabled auto-merge (squash) October 16, 2026 12:06
@fclairamb
fclairamb merged commit d67afc2 into main Oct 16, 2026
26 checks passed
@fclairamb
fclairamb deleted the security/scan-specs-2026-10-16 branch October 16, 2026 12:29
fclairamb added a commit that referenced this pull request Oct 16, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.38.6](v0.38.5...v0.38.6)
(2026-10-16)


### Bug Fixes

* **deps:** update github.com/dop251/goja digest to 2cb8274
([#561](#561))
([085f7de](085f7de))
* **deps:** update go dependencies (non-major)
([#564](#564))
([b91e3d7](b91e3d7))
* **deps:** update module github.com/anthropics/anthropic-sdk-go to
v1.82.0 ([#568](#568))
([c157444](c157444))
* **deps:** update module github.com/gookit/color to v1.6.2
([#566](#566))
([3bf6792](3bf6792))
* **deps:** update module github.com/gookit/color to v1.6.3
([#567](#567))
([f6a33d3](f6a33d3))
* **deps:** update module github.com/quic-go/quic-go to v0.64.0
([#571](#571))
([fd10f2d](fd10f2d))
* no degraded alert right after an outage recovers
([#577](#577))
([c8a2dd2](c8a2dd2))
* security hardening of tokens, invites, secrets, egress and sign-in
(batch 2026-10-16)
([#572](#572))
([d67afc2](d67afc2))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants