AntFleet

Anatomy · 6084d1ea-0

Missing authentication/authorization on secrets API exposes GitHub secret management to any caller

criticalsecurityclosed in 4b9b492
repo 6f7fc663·PR #3·reviewed 1 week ago·closed 1 week ago

The vulnerable code

dashboard/app/api/secrets/route.ts:59-119

59 }
60
61 const setSecrets = new Set(listSecrets())
62
63 // Start with builtin secrets
64 const secrets = BUILTIN_SECRETS.map(s => ({
65 ...s,
66 isSet: setSecrets.has(s.name),
67 }))
68
69 // Add any GitHub secrets not in builtins as custom "Skill Keys"
70 for (const name of setSecrets) {
71 if (!BUILTIN_NAMES.has(name)) {
72 secrets.push({ name, group: 'Skill Keys', description: 'Custom secret', isSet: true })
73 }
74 }
75
76 return NextResponse.json({ secrets, ghReady: true })
77}
78
79export async function POST(request: Request) {
80 if (!ghAvailable()) {
81 return NextResponse.json({ error: 'GitHub CLI not authenticated' }, { status: 503 })
82 }
83
84 const { name, value } = await request.json()
85
86 if (!name || !value) {
87 return NextResponse.json({ error: 'name and value required' }, { status: 400 })
88 }
89
90 // Allow any valid env var name (builtins + custom)
91 if (!VALID_SECRET_NAME.test(name)) {
92 return NextResponse.json({ error: 'Invalid secret name — use UPPER_SNAKE_CASE' }, { status: 400 })
93 }
94
95 try {
96 execFileSync('gh', ['secret', 'set', name, '-b', value], {
97 stdio: 'pipe',
98 cwd: process.cwd(),
99 })
100 return NextResponse.json({ ok: true })
101 } catch (error: unknown) {
102 const msg = error instanceof Error ? error.message : 'Failed to set secret'
103 return NextResponse.json({ error: msg }, { status: 500 })
104 }
105}
106
107export async function DELETE(request: Request) {
108 if (!ghAvailable()) {
109 return NextResponse.json({ error: 'GitHub CLI not authenticated' }, { status: 503 })
110 }
111
112 const { name } = await request.json()
113
114 if (!name || !VALID_SECRET_NAME.test(name)) {
115 return NextResponse.json({ error: 'Invalid secret name' }, { status: 400 })
116 }
117
118 try {
119 execFileSync('gh', ['secret', 'delete', name], { stdio: 'pipe', cwd: process.cwd() })

The reasoning

Opus

Missing authentication/authorization on secrets API exposes GitHub secret management to any caller

criticalsecurityhigh
  • dashboard/app/api/secrets/route.ts:59-119
The route exports GET/POST/DELETE handlers that list, set, and delete GitHub repository secrets via the `gh` CLI. There is no authentication, authorization, CSRF check, origin check, or session validation. If this Next.js dashboard is ever exposed beyond localhost (or reachable via a CSRF/DNS-rebinding/LAN attack from a browser), any caller can enumerate secret names, overwrite any secret matching `[A-Z][A-Z0-9_]+` (including builtins like ANTHROPIC_API_KEY or GH_GLOBAL) with attacker-controlled values, or delete them — silently pivoting CI to attacker credentials. Even on a local dev box this is reachable via CSRF because POST/DELETE bodies are JSON but no `Origin`/`Content-Type` enforcement exists (a form-encoded POST will be parsed by `request.json()` failing, but a fetch from a malicious page with `Content-Type: text/plain` can still send JSON in many configurations; more importantly any tool on the host can hit it).

Recommendation

Require authentication on all three handlers (session/token check, or restrict the route to bind only to loopback and verify Origin/Host headers to mitigate CSRF/DNS-rebinding). At minimum, gate behind an env-token header and reject if request `Origin` is not same-origin.

GPT-5

Secrets API endpoints lack authentication/authorization; unauthenticated clients can list/set/delete repository secrets via GH CLI

criticalsecurityhigh
  • dashboard/app/api/secrets/route.ts:53-77
  • dashboard/app/api/secrets/route.ts:79-105
  • dashboard/app/api/secrets/route.ts:107-125
  • dashboard/app/api/secrets/route.ts:90-93
  • dashboard/app/api/secrets/route.ts:69-74
There is no user authentication or authorization in any handler; the only check is whether the server’s GitHub CLI is authenticated. Any client able to hit these endpoints can enumerate and mutate repository secrets via the server’s GH credentials. The POST path also allows setting arbitrary secret names (not limited to known built-ins), compounding impact.

Recommendation

- Enforce authentication and role-based authorization on all handlers (e.g., require an authenticated session and admin role). - Add CSRF protection if using cookie-based auth. - Restrict allowed secret names (default-deny to builtin names or a server-side allowlist), or gate custom names behind an elevated permission flag. - Consider disabling these endpoints entirely in production builds or behind server-only admin network controls.

The agreement

Both frontier models flagged this within the same line range. AntFleet's unanimous gate fired — the finding posted on the PR. Closed in 4b9b492.

The fix

59 try {
60 const out = execFileSync('gh', ['secret', 'list', ...ghArgsRepo(), '--json', 'name', '-q', '.[].name'], {
61 stdio: 'pipe',
62 cwd: process.cwd(),
63 }).toString().trim()
64 return out ? out.split('\n').filter(Boolean) : []
65 } catch {
66 return []
67 }
68}
69
70export async function GET() {
71 if (!ghAvailable()) {
72 return NextResponse.json({
73 error: 'GitHub CLI not authenticated. Run: gh auth login',
74 ghReady: false,
75 }, { status: 503 })
76 }
77
78 const setSecrets = new Set(listSecrets())
79
80 // Start with builtin secrets
81 const secrets = BUILTIN_SECRETS.map(s => ({
82 ...s,
83 isSet: setSecrets.has(s.name),
84 }))
85
86 // Add any GitHub secrets not in builtins as custom "Skill Keys"
87 for (const name of setSecrets) {
88 if (!BUILTIN_NAMES.has(name)) {
89 secrets.push({ name, group: 'Skill Keys', description: 'Custom secret', isSet: true })
90 }
91 }
92
93 return NextResponse.json({ secrets, ghReady: true })
94}
95
96export async function POST(request: Request) {
97 if (!ghAvailable()) {
98 return NextResponse.json({ error: 'GitHub CLI not authenticated' }, { status: 503 })
99 }
100
101 const { name, value } = await request.json()
102
103 if (!name || !value) {
104 return NextResponse.json({ error: 'name and value required' }, { status: 400 })
105 }
106
107 // Allow any valid env var name (builtins + custom)
108 if (!VALID_SECRET_NAME.test(name)) {
109 return NextResponse.json({ error: 'Invalid secret name — use UPPER_SNAKE_CASE' }, { status: 400 })
110 }
111
112 try {
113 execFileSync('gh', ['secret', 'set', name, ...ghArgsRepo(), '-b', value], {
114 stdio: 'pipe',
115 cwd: process.cwd(),
116 })
117 return NextResponse.json({ ok: true })
118 } catch (error: unknown) {
119 const msg = error instanceof Error ? error.message : 'Failed to set secret'

Closure

Closed 1 week ago

SHA: 4b9b49251c8c9808bf147d55aa2930352af2e8c0

View closure receipt on GitHub →

Tweet thread template

tweet 1 of 8187 / 280

Two frontier models reviewed PR #3 on 6f7fc663. Both found this bug: critical security: Missing authentication/authorization on secrets API exposes GitHub secret management to any caller

tweet 2 of 8127 / 280

The vulnerable code (dashboard/app/api/secrets/route.ts:59-119): (full snippet at https://www.antfleet.dev/anatomy/6084d1ea-0)

tweet 3 of 8280 / 280

What Opus saw: "The route exports GET/POST/DELETE handlers that list, set, and delete GitHub repository secrets via the `gh` CLI. There is no authentication, authorization, CSRF check, origin check, or session validation. If this Next.js dashboard is ever exposed beyond localho…

tweet 4 of 8280 / 280

What GPT-5 saw: "There is no user authentication or authorization in any handler; the only check is whether the server’s GitHub CLI is authenticated. Any client able to hit these endpoints can enumerate and mutate repository secrets via the server’s GH credentials. The POST pat…

tweet 5 of 897 / 280

Both flagged the same line range. AntFleet's unanimous gate fired — the finding posted on the PR.

tweet 6 of 893 / 280

The fix landed in commit 4b9b492: (view diff at https://www.antfleet.dev/anatomy/6084d1ea-0)

tweet 7 of 881 / 280

AntFleet reviews every PR with two frontier models. Only unanimous findings post.

tweet 8 of 877 / 280

Full anatomy + reasoning + diffs: https://www.antfleet.dev/anatomy/6084d1ea-0

Paste into X composer one tweet at a time. X has no multi-tweet intent API.