Skip to content

[codex] Require CSRF for state-changing APIs#442

Merged
cssbruno merged 1 commit into
mainfrom
codex/security-431-437-check
Jun 15, 2026
Merged

[codex] Require CSRF for state-changing APIs#442
cssbruno merged 1 commit into
mainfrom
codex/security-431-437-check

Conversation

@cssbruno

Copy link
Copy Markdown
Owner

Summary

  • Protect generated state-changing API handlers with the generated CSRF validator before request body wrapping and user handler execution.
  • Mark unsafe API endpoints as CSRF-protected in IR/security metadata and add an audit_api_missing_csrf baseline diagnostic when CSRF is disabled.
  • Update API/security docs to reflect JSON content-type enforcement and generated CSRF for unsafe APIs.

Closes #433.

Verification

  • go test ./internal/gwdkir ./internal/gwdkanalysis ./internal/appgen ./internal/compiler ./internal/auditspec ./internal/diagnostics
  • go build ./cmd/gowdk
  • scripts/test-go-modules.sh

Notes

I checked #431, #432, #434, #435, #436, and #437 against current main; those fixes are already present there. PR #440 is already merged with green checks.

@cssbruno cssbruno marked this pull request as ready for review June 15, 2026 14:02
@cssbruno cssbruno merged commit 87920b9 into main Jun 15, 2026
5 checks passed
@cssbruno cssbruno deleted the codex/security-431-437-check branch June 15, 2026 14:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12f4ff62c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +76 to 78
stmts = append(stmts, apiCSRFStmts(csrf)...)
stmts = append(stmts,
assign([]ast.Expr{selExpr(id("request"), "Body")}, call(sel("http", "MaxBytesReader"), id("response"), selExpr(id("request"), "Body"), id("maxAPIBodyBytes"))),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cap API bodies before CSRF validation

When a state-changing API request has the CSRF cookie but omits the CSRF header, CSRF.Validate falls back to request.ParseForm() (addons/actions/csrf.go:163-168). Because this new API CSRF block runs before wrapping request.Body with http.MaxBytesReader, form-encoded cross-site requests bypass the configured maxAPIBodyBytes cap during CSRF validation before the request is rejected. Move the MaxBytesReader assignment ahead of CSRF validation or make the validator avoid parsing an uncapped API body.

Useful? React with 👍 / 👎.

Comment thread internal/appgen/source.go
func csrfEnabled(options Options) bool {
adapter := backendAdapterIR(options)
return options.Config.Build.CSRF.EnabledForGeneratedEndpoints() && (adapter.HasEndpointKind(BackendEndpointAction) || contractExposuresParseForm(executableContractExposures(adapter.ContractExposures)))
return options.Config.Build.CSRF.EnabledForGeneratedEndpoints() && (adapter.HasEndpointKind(BackendEndpointAction) || adapter.HasCSRFSensitiveAPI() || contractExposuresParseForm(executableContractExposures(adapter.ContractExposures)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Provide a token bootstrap for API CSRF

Enabling generated CSRF solely because a state-changing API exists makes browser fetch/XHR clients send X-GOWDK-CSRF, but the current token source is only exercised by CSRFInjectHTML, which returns without minting a cookie unless a GET HTML response contains a POST form (runtime/app/app.go:381-388,400-408), and the cookie itself is HttpOnly. In an API-only flow or a page that posts via JS without a generated POST form, clients never receive a token to echo, so every legitimate POST/PUT/DELETE API call is rejected with 403; add a generated token bootstrap such as a meta/header/endpoint for API CSRF.

Useful? React with 👍 / 👎.

if csrfRule.Code == "" {
csrfRule.Code = csrfCodeForKind(endpoint.Kind)
}
if policy.Builtin && endpoint.Kind == "api" && csrfRule.Code == "audit_api_missing_csrf" && !gwdkir.HTTPMethodRequiresCSRF(endpoint.Method) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve safe-API exemption for extended baselines

When a project declares a policy that extends "baseline.api", the inherited audit_api_missing_csrf rule is evaluated under the child policy with Builtin == false, so this guard no longer skips GET/HEAD-style APIs and gowdk audit starts reporting missing CSRF for read-only APIs. Since docs/language/audit.md documents extending built-ins as normal composition, keep the state-changing-method exemption attached to the API CSRF rule itself rather than only to policies marked built-in.

Useful? React with 👍 / 👎.

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.

[Security] API endpoints perform no CSRF validation and accept requests with an empty Content-Type

1 participant