Skip to content

v0.28.0: rate limits, security events and remaining review findings - #72

Merged
grokify merged 37 commits into
mainfrom
release/v0.28.0
Oct 29, 2026
Merged

grokify merged 37 commits into
mainfrom
release/v0.28.0

Conversation

@grokify

@grokify grokify commented Oct 29, 2026

Copy link
Copy Markdown
Member

The rest of the production-hardening initiative: Phase 4 and Phase 5 of docs/specs/initiatives/INIT-SYSTEMFORGE-015/ROADMAP.md. Release notes and upgrade guide: docs/releases/v0.28.0.md.

Security

  • One client-address rule (session/clientaddr): trusted proxy networks and the header to read, used by SystemAuth, the BFF, appauth, rate limits and security events. Headers from untrusted peers are ignored.

  • Layered rate limits, on by default in SystemAuth:

    • a default per-address limit on every route;
    • route policies for the token, login, sign-up and BFF sign-in paths;
    • limits on failed client authentication, the JWT-bearer grant and social login;
    • a limit for signed-in callers.

    429 responses carry Retry-After (never 0) and RateLimit-Policy/RateLimit. When storage fails, the default layer lets requests through and credential limits refuse.

  • Logs fit for incidents:

    • OAuth errors are logged as text summaries without stack traces.
    • Tests run real flows and assert that no tokens or secrets reach logs or events.
    • Structured security events go through contract/audit, with request IDs and the visitor's address (docs/security/security-events.md).
  • No internal error text in API responses: about 60 sites across organization, marketplace, contract, invite, connections, session middleware, managed principals, federation and the sign-up API.

  • Static API key authentication removed from relying parties. Use service accounts or identity/publicclient.

  • Smaller findings:

    • the BFF cookie refuses SameSite=None;
    • EmbeddedOAuthProvider.Revoke now revokes;
    • every server sets ReadHeaderTimeout.
  • Threat model: docs/security/threat-model.md.

Breaking

  • API key options removed.
  • Rate limits on by default (rate_limit.disabled to opt out).
  • Some organization status codes changed.
  • contract.ToContractError no longer includes error text.
  • The BFF cookie rejects SameSite=None.

See the upgrade guide. Behind a reverse proxy, set client_address, or every visitor shares the proxy's address.

Tests, race tests on SystemAuth and sessions, and lint pass locally. The publication scan is clean.

🤖 Generated with Claude Code

grokify and others added 30 commits October 28, 2026 18:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Programmatic clients authenticate with DPoP-bound SystemAuth access
tokens: command-line tools sign people in with identity/publicclient and
automation uses service accounts (ADR-016). An opaque Bearer value is now
an invalid token and the X-API-Key header is not a credential.

The identity/apikey package and the session/middleware API key
middleware are unchanged.

BREAKING CHANGE: appauth.Options.APIKeys, relyingparty.BearerConfig.APIKeys,
relyingparty.APIKeyValidator, relyingparty.AuthMethodAPIKey and
relyingparty.AuthenticatedPrincipal.APIKeyID are removed. Remove the
APIKeys option and move callers to publicclient sign-in or service
accounts.

Refs: RMI-SYSTEMFORGE-217
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add an upgrade section to the application authentication guide and drop
API keys from the relying-party, AAuth and README descriptions.

Refs: RMI-SYSTEMFORGE-217
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the scope and assumptions, assets, actors and trust boundaries,
a STRIDE table of threats per boundary with the mitigation and the
package that holds it, the residual risks and accepted trade-offs, and
how the model is kept current. Link it from the security policy, the
production checklist and the Security section of the site navigation.

Refs: RMI-SYSTEMFORGE-217
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rate limits

Add session/clientaddr: a Resolver configured with trusted proxy
networks and the header they write (X-Forwarded-For, read from the
right, or a single-value header such as CF-Connecting-IP), falling back
to the direct peer. Headers from peers outside the trusted networks are
never believed.

SystemAuth reads it from the new client_address configuration (or
WithClientAddressResolver) and uses it for login-token, waitlist and
sign-up limits, recorded key-use addresses and SCIM audit events.
TrustedProxyNetworks, ratelimit.PeerIP and ForwardedClientIP, the BFF
client IP extractor and the API key middleware are built on it.
ForwardedClientIP, ForwardedIPKey, WithLoginTokenClientIP,
CloudflareClientIPConfig and the legacy ClientIPConfig fields are
deprecated in favour of a Resolver; ratelimit.AddressKey keys limits on
it.

Refs: RMI-SYSTEMFORGE-213
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-213
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add ratelimit.Policy, plain net/http middleware applying a default
limit per client address on every route and a route table: the token
endpoint per address and per OAuth client, login, sign-up and BFF
authentication paths per address, and exemptions for health checks,
static assets and the payment webhook. Route entries are replaced by
name with MergeRoutes; back-channel networks can be exempted.

NewSignedInLimiter limits signed-in principals and service accounts
after authentication. Check, Exhausted and Record give handlers limits
on the same storage, and MemoryStorage and RedisStorage can be peeked
without counting.

Responses carry the IETF RateLimit-Policy and RateLimit headers beside
X-RateLimit-*, and Retry-After is rounded up to at least one second.
The default layer fails open and logs on a storage outage while route
limits fail closed; a full MemoryStorage is always a 429.

Refs: RMI-SYSTEMFORGE-214
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SystemAuth installs the default layer and route policies outermost,
configured under rate_limit (default, signed_in, routes, exempt_networks,
disabled, redis_url, single_instance). The login-token, waitlist and
sign-up page limits and the managed-principal code limits move onto the
shared storage. New handler limits cover failed client authentication
at the token endpoint, tokens issued per service account by the
JWT-bearer grant, and social login start and callback. The
managed-principal API gets the signed-in limit after authentication.

systemauthsvc connects rate_limit.redis_url or takes
Options.RateLimitStorage, and warns in production when counters are
per instance without a declared single instance. appauth accepts a
client-address resolver and a signed-in limiter for its middleware.

Refs: RMI-SYSTEMFORGE-214
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-214
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a guide to the client-address rule and the rate limit layers, and
update the deployment guide, the production checklist, the BFF and
rate limiting pages for client_address and rate_limit.

Refs: RMI-SYSTEMFORGE-213
Refs: RMI-SYSTEMFORGE-214
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CookieConfig.Validate, and so NewHandler, now rejects SameSite=None: the
origin check that protects state-changing requests relies on the browser
withholding the session cookie from cross-site subrequests. An unset or
http.SameSiteDefaultMode SameSite still defaults to Strict. The other
session cookie writers in systemauth, relyingparty and oauthclient
already hard-code or require Lax.

Refs: RMI-SYSTEMFORGE-218
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revoke passed the raw token where storage expects a Fosite request ID,
so it reported success without revoking anything. It now looks the
access or refresh token up through Fosite's introspection, revokes the
access and refresh tokens of its family, and returns an error wrapping
ErrInvalidToken when the token is unknown, expired or already revoked.

Refs: RMI-SYSTEMFORGE-219
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… builds

The multiapp server now bounds the time a client may take to send its request headers, and the publicclient loopback callback server sets read, write and idle timeouts alongside its header timeout. cmd/systemauth already set all four.

Refs: RMI-SYSTEMFORGE-220
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…their text

Internal logs an unexpected error and returns a Huma 500 with a generic message; Log does the same for net/http handlers. Huma copies an error passed to its constructors into the response body, so handlers must not pass internal errors to it.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Known service errors map to fixed messages and statuses; anything else is logged and answered with a generic 500. The ad-hoc validation errors of the service now wrap new sentinels (ErrOwnerRequired, ErrAlreadyMember, ErrNewOwnerNotMember, ErrInvalidOwnerRole) or existing ones (ErrInvalidSlug, ErrNotOwner, ErrOrganizationNotFound, ErrMembershipNotFound), so their messages stay available to clients.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Listing service errors a client can act on map to fixed messages; anything else is logged and answered with a generic 500. Ent validation failures on create and update now wrap the new ErrInvalidListing, and deleting a published listing wraps the new ErrListingNotDeletable.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Identity and policy operations log provider failures and answer a generic 500 instead of passing the error to Huma. The token middleware answers "invalid token" and logs the reason, and ToContractError no longer copies a non-contract error's text into its message.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Known invite, organization, authorization and application role errors map to fixed messages; anything else is logged and answered with a generic 500. The service's ad-hoc validation errors now wrap new sentinels (ErrInviteNotFound, ErrPendingInviteExists, ErrAlreadyMember, ErrAppRolesNotConfigured, ErrCollaboratorAppRoles).

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onse

Known waitlist failures keep their message; any other failure is logged and reported as "internal error".

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Credential, scope, rate-limit, transient and conflict errors answer fixed messages and log the detail; a transient error could otherwise carry the provider's network error text.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bearer and DPoP middlewares send the matching sentinel's text instead of the full error chain, so parser, key and replay-cache details stay out of 401 bodies; an unexpected error is logged and answered "authentication failed".

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onses

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Known sync failures keep their message; storage errors are logged and answered generically.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Token, authorize and access-token log records now carry the RFC 6749
error code, description, hint and status of a refused request instead
of the error value, which slog's text handler formats with %+v. The
wrapped cause and Fosite's debug text are added only when the logger is
enabled at debug level, or for server errors, and always by message.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
audit.Context gains RequestID, filled from chi's request-ID middleware
by audit.RequestID. NopEmitter is the default where an emitter is
optional, and LogEmitter writes each event as one structured log
record, leaving out the session ID. requestprincipal.AuditActor names
the request's principal as an event's actor.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WithAuditEmitter sets where SystemAuth records sign-ins and their
refusals, sign-outs, refused token requests, refresh-token reuse,
authorization-code replay, revocations of tokens and token families,
and the signing key in use at startup. Each event carries the request
ID, the principal or client ID and the client address, taken from one
resolver, and never a token, code or secret. Events are discarded by
default.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… changes

organization.WithAuditEmitter records memberships added, updated,
removed and ownership transfers; principal.WithAuditEmitter records
platform administration granted and revoked; serviceaccount.Config.Audit
records account, key and role changes, by key ID only. The actor is the
request's principal, or the operator the call names.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…waivers

Options.AuditEmitter now reaches SystemAuth and the organization and
principal services the package builds, and every production check
waived at startup is recorded as a systemauth.production.waiver event.
The systemauth command writes its security events to its log.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real flows run through an embedded SystemAuth logging at debug level to
text and JSON handlers: social login, the authorization code flow with
PKCE, refresh, code and refresh-token replay, client credentials with a
right and a wrong secret, JWT bearer, revocation, logout, and BFF
sign-in and sign-out. No token, code, verifier, client or HMAC secret,
DPoP proof, assertion, key or session cookie may appear in any log
attribute or event. Further tests cover each security event.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
grokify and others added 6 commits October 28, 2026 20:30
Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd a proxy

Security events took the client address from the request's direct
peer, so behind a reverse proxy every event recorded the proxy. They now
use the server's client-address rule, the same one rate limits use.

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-217
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…address rule

Refs: RMI-SYSTEMFORGE-215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-221
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 136 files, which is 36 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 317d19a3-f0f2-41a5-b6b2-b0d2a8a9c0f4
Commits

Reviewing files that changed from the base of the PR and between 9dc7d57 and 22bf9e4.

Files selected for processing: 136
  • CHANGELOG.json
  • CHANGELOG.md
  • README.md
  • SECURITY.md
  • cmd/systemauth/app.go
  • connections/http.go
  • connections/http_errors_internal_test.go
  • contract/audit/emitter.go
  • contract/audit/emitter_test.go
  • contract/audit/store.go
  • contract/errors.go
  • contract/internal_error_test.go
  • contract/middleware.go
  • contract/operations_identity.go
  • contract/operations_policy.go
  • docs/bff/overview.md
  • docs/coreapi/ratelimit.md
  • docs/design/architecture/managed-principals.md
  • docs/guides/app-authentication.md
  • docs/guides/rate-limiting.md
  • docs/identity/ai-agents-aauth.md
  • docs/releases/changelog.md
  • docs/releases/index.md
  • docs/releases/v0.28.0.md
  • docs/security/production-checklist.md
  • docs/security/security-events.md
  • docs/security/threat-model.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-015/ROADMAP.md
  • docs/systemauth/deployment.md
  • docs/systemauth/relying-party.md
  • identity/appauth/appauth.go
  • identity/internal/oauthcore/logerr.go
  • identity/internal/oauthcore/logerr_test.go
  • identity/invite/error_leak_test.go
  • identity/invite/errors.go
  • identity/invite/handler_invites.go
  • identity/invite/invite.go
  • identity/invite/service.go
  • identity/managedprincipal/handler.go
  • identity/managedprincipal/handler_errors_test.go
  • identity/oauth/api.go
  • identity/oauth/handler_fosite.go
  • identity/organization/audit.go
  • identity/organization/audit_test.go
  • identity/organization/error_leak_test.go
  • identity/organization/errors.go
  • identity/organization/handler_members.go
  • identity/organization/handler_orgs.go
  • identity/organization/rename.go
  • identity/organization/service.go
  • identity/principal/audit.go
  • identity/principal/audit_test.go
  • identity/principal/service.go
  • identity/publicclient/callback_server_internal_test.go
  • identity/publicclient/publicclient.go
  • identity/relyingparty/middleware.go
  • identity/relyingparty/middleware_test.go
  • identity/relyingparty/requestprincipal.go
  • identity/relyingparty/requestprincipal_test.go
  • identity/relyingparty/security_log_test.go
  • identity/requestprincipal/aauthrs/aauthrs_test.go
  • identity/requestprincipal/audit.go
  • identity/scim/scimtoken/credential.go
  • identity/scim/scimtoken/service.go
  • identity/serviceaccount/account.go
  • identity/serviceaccount/audit.go
  • identity/serviceaccount/audit_test.go
  • identity/serviceaccount/key.go
  • identity/serviceaccount/role.go
  • identity/serviceaccount/serviceaccount.go
  • identity/signupgate/signupapi/error_leak_test.go
  • identity/signupgate/signupapi/signupapi.go
  • identity/systemauth/client_address.go
  • identity/systemauth/client_address_test.go
  • identity/systemauth/config.go
  • identity/systemauth/federation_handlers.go
  • identity/systemauth/federation_sync_errors_test.go
  • identity/systemauth/handler_fosite.go
  • identity/systemauth/jwt_bearer.go
  • identity/systemauth/login_token_exchange.go
  • identity/systemauth/login_token_exchange_test.go
  • identity/systemauth/principal_status.go
  • identity/systemauth/provider_oauth.go
  • identity/systemauth/provider_oauth_revoke_test.go
  • identity/systemauth/ratelimit.go
  • identity/systemauth/ratelimit_test.go
  • identity/systemauth/schema/config.schema.json
  • identity/systemauth/schema/generate.go
  • identity/systemauth/security_events.go
  • identity/systemauth/security_events_address_test.go
  • identity/systemauth/security_events_login.go
  • identity/systemauth/security_events_test.go
  • identity/systemauth/security_log_test.go
  • identity/systemauth/server.go
  • identity/systemauth/session.go
  • identity/systemauth/signup_code_page.go
  • identity/systemauth/signup_waitlist.go
  • identity/systemauth/social_login.go
  • identity/systemauth/social_logout.go
  • identity/systemauthsvc/config.go
  • identity/systemauthsvc/managedprincipals.go
  • identity/systemauthsvc/production.go
  • identity/systemauthsvc/production_test.go
  • identity/systemauthsvc/ratelimit.go
  • identity/systemauthsvc/ratelimit_test.go
  • identity/systemauthsvc/scim.go
  • identity/systemauthsvc/service.go
  • internal/apierror/apierror.go
  • internal/apierror/apierror_test.go
  • marketplace/access.go
  • marketplace/access_test.go
  • marketplace/errors.go
  • marketplace/handlers.go
  • marketplace/internal_error_test.go
  • marketplace/listing_service.go
  • mkdocs.yml
  • multiapp/server.go
  • multiapp/server_timeouts_test.go
  • session/bff/clientip.go
  • session/bff/clientip_test.go
  • session/bff/cookie.go
  • session/bff/cookie_prefix_test.go
  • session/clientaddr/clientaddr.go
  • session/clientaddr/clientaddr_test.go
  • session/middleware/apikey.go
  • session/middleware/dpop.go
  • session/middleware/error_text_test.go
  • session/middleware/http.go
  • session/ratelimit/clientip.go
  • session/ratelimit/headers.go
  • session/ratelimit/keyfunc.go
  • session/ratelimit/memory.go
  • session/ratelimit/policy.go
  • session/ratelimit/policy_test.go
  • session/ratelimit/ratelimit.go
  • session/ratelimit/redis.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread connections/http.go
case errors.Is(err, cf.ErrInvalidCredential):
// A provider's error can name hosts or carry its own text: the
// client gets a fixed message and the detail is logged.
h.log.InfoContext(c.r.Context(), "connections: provider rejected the credential", "error", err)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

Comment thread connections/http.go
h.log.InfoContext(c.r.Context(), "connections: provider rejected the credential", "error", err)
writeError(c.w, http.StatusUnprocessableEntity, "the provider rejected the credential")
case errors.Is(err, cf.ErrInsufficientScope):
h.log.InfoContext(c.r.Context(), "connections: credential lacks scope", "error", err)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

Comment thread connections/http.go
h.log.InfoContext(c.r.Context(), "connections: credential lacks scope", "error", err)
writeError(c.w, http.StatusUnprocessableEntity, "the credential lacks a required scope")
case errors.Is(err, cf.ErrRateLimited):
h.log.WarnContext(c.r.Context(), "connections: provider rate limited", "error", err)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

Comment thread connections/http.go
writeError(c.w, http.StatusBadGateway, "the provider is limiting requests; try again later")
case errors.Is(err, cf.ErrTransient):
// A transient error wraps the network or provider failure.
h.log.WarnContext(c.r.Context(), "connections: provider unavailable", "error", err)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

Comment thread contract/audit/emitter.go
if len(ev.Details) > 0 {
attrs = append(attrs, slog.Any("details", ev.Details))
}
e.logger.LogAttrs(ctx, level, "security event", attrs...)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

// already bound to a key needs a proof by it.
if err := s.bindDPoP(ar, proof); err != nil {
logger.Info("token request rejected: DPoP", "grant_type", grantType, "client_id", clientID, "error", err)
logger.Info("token request rejected: DPoP", "grant_type", grantType, "client_id", clientID, oauthcore.ErrorAttr(ctx, logger, err))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

// active, and its token family is revoked.
if err := s.requireActiveSubject(ctx, ar); err != nil {
logger.Info("token request rejected", "grant_type", grantType, "client_id", clientID, "error", err)
logger.Info("token request rejected", "grant_type", grantType, "client_id", clientID, oauthcore.ErrorAttr(ctx, logger, err))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

// Bound refresh-token rotation by the family's absolute lifetime.
if err := s.applyTokenFamilyLifetime(ctx, ar); err != nil {
logger.Info("token request rejected", "grant_type", grantType, "client_id", clientID, "error", err)
logger.Info("token request rejected", "grant_type", grantType, "client_id", clientID, oauthcore.ErrorAttr(ctx, logger, err))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

Comment thread identity/systemauth/server.go Fixed
if logger == nil {
logger = slog.Default()
}
logger.ErrorContext(ctx, op, "error", err)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exploitable as log injection: this is a log/slog call, and the value is one structured attribute. slog's text handler quotes any value containing spaces, '=', quotes or control characters (including newlines), and the JSON handler escapes them, so a value cannot start a forged log line. The attribute is kept on purpose: the error text or client and grant identifiers are what an operator needs to investigate a refused request. No change.

The access-token middleware and the managed-principal handler logged
r.URL.Path, which the client chooses; they now log the route the
request matched.

Refs: RMI-SYSTEMFORGE-216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
writeError(w, http.StatusConflict, msg)
return
}
h.svc.logger.ErrorContext(r.Context(), "managedprincipal: request failed", "route", chi.RouteContext(r.Context()).RoutePattern(), "error", err)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request path on this line was replaced by the matched route in 22bf9e4; this alert is the remaining source, the error value. Not exploitable as log injection: it is one log/slog structured attribute, which slog's text handler quotes (including any newline) and the JSON handler escapes, so it cannot start a forged log line. The error is kept because it is what an operator needs to investigate a failed request. No change.

@grokify
grokify merged commit 38fa420 into main Oct 29, 2026
10 checks passed
@grokify
grokify deleted the release/v0.28.0 branch October 29, 2026 11:05
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