Skip to content

fix(api): answer 400 or 404, not 500, for a repo claim or key with a NUL byte - #792

Merged
ThomasK33 merged 8 commits into
mainfrom
auth-repo-claim-nul
Nov 10, 2026
Merged

ThomasK33 merged 8 commits into
mainfrom
auth-repo-claim-nul

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Why

A request whose repository name or id held a NUL byte answered 500 on PostgreSQL. PostgreSQL refuses a NUL byte or invalid UTF-8 in a text parameter (SQLSTATE 22021). Two inputs carried such text into a repository lookup or create:

  • A JWT repo claim with a NUL. On PostgreSQL, POST /api/v1/repos, GET /api/v1/repo, GET /api/v1/repos/branches and DELETE /api/v1/repos/delete each answered 500. SQLite created such a repository (201).
  • A path name or id with a NUL or invalid UTF-8, such as GET /api/repos/a%00b or GET /api/repo-urls/a%00b. PostgreSQL answered 500 and SQLite 404.

A claim-checking lane found the first while #790 (issue #788) was in review. That PR's advisor found the second.

What changes

  • POST /repos (both paths, every body form) answers 400 bad_request "invalid repo claim (want UTF-8 text without NUL)" when the repo claim is not meta.ValidText. The claim becomes the new repository's url, so no backend creates a repository with such a url. The check is in forkAwareCreateRepo, which every create form passes through. JSON decoding already makes a claim valid UTF-8, so in practice this refuses a NUL.
  • PostgreSQL's GetRepoByURL, GetRepoByID and SoftDeleteRepo answer ErrNotFound for a key that meta.ValidText refuses, before any query: PostgreSQL holds no such row. Every route that looks a repository up by the claim or by a path name or id then answers its existing 404 on REST instead of 500. The git surface already answered 403 (claim mismatch) and still does.
  • meta.ValidText (new, internal/meta/text.go): valid UTF-8 with no NUL.
  • SQLite and S3-WAL lookups are unchanged. A repository that either one created for a NUL claim before this change keeps working with that claim, as on main, upstream sync included. Only a repeat POST /repos with that claim changes: it answered by the row's state (200 for a finished repository, 409, 503 or 404 otherwise) and now answers 400.
  • Docs: docs/reference/authentication.md states the create rule. The internal/api and internal/meta READMEs state the create rule and the PostgreSQL rule. The internal/auth README drops a stale sub == SubjectLiteral invariant: any non-empty sub verifies since fix(auth): accept any non-empty sub and treat a token without repo as org-wide #666.

Other request text with a NUL byte (webhook and git-credential fields and ids, cursors, sync bodies) is #793.

Verification

  • TestRepoClaimWithNUL (PostgreSQL and SQLite): create answers 400 "invalid repo claim (want UTF-8 text without NUL)" on /api/v1/repos, /api/repos and with a fork body and a copy body (the same detail, so the check runs before the fork dispatch). A claim with control characters, é and U+FFFD still creates (201). GET /repo, GET /repos/branches and DELETE /repos/delete answer 404. On SQLite, a repository stored with such a url still answers 200 to GET /repo and to delete.
  • TestAliasNotFound gains GET /api/repos/a%00b and a%FFb (404). TestRepoURLByID gains GET /api/repo-urls/a%00b and a%FFb (404). TestResolveRepoKey gains team/a\x00b and team/a\xffb (ErrNotFound on every backend), and resolves urls with control characters, é and U+FFFD on every backend.
  • TestPGRepoLookupsRefuseInvalidTextWithoutQuery calls the three PostgreSQL methods on a PG with no pool, so any database access panics: such keys answer ErrNotFound.
  • Mutation: without the create check, create answers 500 on PostgreSQL and 201 on SQLite. Without the SoftDeleteRepo guard, delete answers 500 on PostgreSQL. Without the GetRepoByURL or GetRepoByID guard, TestResolveRepoKey/pg fails with SQLSTATE 22021, and TestAliasNotFound/pg (both) and TestRepoURLByID/pg (id) fail. With the GetRepoByURL guard after lookupOrgID, even before its error check, that test panics. A ValidText that also refuses \x01 or é fails TestResolveRepoKey/pg.
  • golangci-lint 2.15.0 reports 0 issues. go vet, scripts/check-doc-links.py (broken links: 0) and both --fingerprints-only checks pass.

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

A repo claim with a NUL byte answered 500 on PostgreSQL for every
repository route, because PostgreSQL refuses such text. SQLite stored
it. The verifier now refuses such a claim, as it already refuses an iss
with a NUL.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I62ffb423af933aa2a01f855f593e69f7996bae59
Signed-off-by: Thomas Kosiewski <tk@coder.com>
A path name such as /api/repos/a%00b reached PostgreSQL and answered
500. No repository can have such a key, so ResolveRepoKey answers
ErrNotFound without a store call, on every backend.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: Ic6e1c02e480c4f801a0fe56d73185fc83c7bd56d
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Move the NUL and UTF-8 guard from ResolveRepoKey into the PostgreSQL
GetRepoByURL and GetRepoByID. SQLite and S3-WAL can hold such a url
from before the verifier refused NUL claims, so they keep resolving it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I8094fe392a757991b0bcea3523b8b1bb160b5b3e
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…legacy rows

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: Ibfaf7345017de243987b93541ce228737d1b4547
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T20:03:36.678333Z e37c682 Manual request
🔒 Security Review ✅ Completed 2026-11-10T20:04:17.296509Z e37c682 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 383b146a97

ℹ️ 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".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 383b146a97

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

… the legacy note

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I03de578b58b843f4745bd88a41b727e300f1110a
Signed-off-by: Thomas Kosiewski <tk@coder.com>
The verifier refusal locked out repositories that SQLite and S3-WAL had
stored with such a url, and broke their upstream sync (the loopback
token carries the url). Refuse the claim only where it would name a new
repository, and answer not found on PostgreSQL for lookups and delete.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I419eb5f0f6d7bfad44f18414b9031160ad72c4b1
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33 ThomasK33 changed the title fix(auth): refuse a NUL byte in a repository name from a token or a path fix(api): answer 400 or 404, not 500, for a repo claim or key with a NUL byte Nov 10, 2026
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: c1d2980c44

ℹ️ 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".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: c1d2980c44

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…tabase access

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: Ib423276f53400246bee1713f592c15ba02dd92e8
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…or valid unusual claims

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I6956696b39f508fb439b0287a74e40acfac74e44
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: e37c682197

ℹ️ 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".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: e37c682197

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit d5ce6f0 Nov 10, 2026
11 of 12 checks passed
@ThomasK33
ThomasK33 deleted the auth-repo-claim-nul branch November 10, 2026 21:01
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.

1 participant