AntFleet

Anatomy · 6017bf3f-0

Missing authentication/authorization on secret-management endpoints

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

The vulnerable code

dashboard/app/api/secrets/route.ts:95-145

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'
120 return NextResponse.json({ error: msg }, { status: 500 })
121 }
122}
123
124export async function DELETE(request: Request) {
125 if (!ghAvailable()) {
126 return NextResponse.json({ error: 'GitHub CLI not authenticated' }, { status: 503 })
127 }
128
129 const { name } = await request.json()
130
131 if (!name || !VALID_SECRET_NAME.test(name)) {
132 return NextResponse.json({ error: 'Invalid secret name' }, { status: 400 })
133 }
134
135 try {
136 execFileSync('gh', ['secret', 'delete', name, ...ghArgsRepo()], { stdio: 'pipe', cwd: process.cwd() })
137 return NextResponse.json({ ok: true })
138 } catch (error: unknown) {
139 const msg = error instanceof Error ? error.message : 'Failed to delete secret'
140 return NextResponse.json({ error: msg }, { status: 500 })
141 }
142}
143

The reasoning

Opus

Missing authentication/authorization on secret-management endpoints

criticalsecurityhigh
  • dashboard/app/api/secrets/route.ts:95-145
  • dashboard/app/api/auth/route.ts:50-110
Both route handlers expose the ability to read which secrets are set, create/overwrite arbitrary GitHub Actions secrets (including ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN), and delete them. There is no authentication, no session check, no CSRF token, and no origin/host verification. If the Next.js dashboard is ever exposed beyond localhost (or reachable from a browser visiting a malicious page that POSTs JSON with a simple Content-Type to bypass CORS preflight is harder, but a curl/proxy or LAN attacker still trivially exploits it), an attacker can overwrite repo-level secrets with attacker-controlled values, exfiltrate via subsequent workflow runs, or wipe production credentials. Even on localhost, any other process on the machine can hit the endpoint. There is also no rate limiting.

Recommendation

Require authentication for all mutating routes (and arguably GET, which leaks which secrets exist). At minimum, bind the dev server to 127.0.0.1, verify a same-origin/Host header, require a CSRF token, and gate behind a session cookie or local-only token. Also reject requests if NODE_ENV === 'production' unless an explicit admin gate is satisfied.

GPT-5

Secrets endpoint passes secret value via command-line args (-b), exposing secrets to other processes

highsecurityhigh
  • dashboard/app/api/secrets/route.ts:112-116
  • dashboard/app/api/auth/route.ts:63-66
In POST /api/secrets, the secret value is supplied using '-b', which puts the secret on the process command line (argv). On many systems, process arguments are visible to other users and may be logged, leaking the secret. The auth route correctly uses stdin to provide the secret, avoiding this leak.

Recommendation

Provide the secret via stdin instead of '-b'. For example: execFileSync('gh', ['secret','set', name, ...ghArgsRepo()], { input: value, stdio: ['pipe','pipe','pipe'] }). Also ensure no logging of the value.

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

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'
120 return NextResponse.json({ error: msg }, { status: 500 })
121 }
122}
123
124export async function DELETE(request: Request) {
125 if (!ghAvailable()) {
126 return NextResponse.json({ error: 'GitHub CLI not authenticated' }, { status: 503 })
127 }
128
129 const { name } = await request.json()
130
131 if (!name || !VALID_SECRET_NAME.test(name)) {
132 return NextResponse.json({ error: 'Invalid secret name' }, { status: 400 })
133 }
134
135 try {
136 execFileSync('gh', ['secret', 'delete', name, ...ghArgsRepo()], { stdio: 'pipe', cwd: process.cwd() })
137 return NextResponse.json({ ok: true })
138 } catch (error: unknown) {
139 const msg = error instanceof Error ? error.message : 'Failed to delete secret'
140 return NextResponse.json({ error: msg }, { status: 500 })
141 }
142}
143

Closure

Closed 1 week ago

SHA: 4b9b49251c8c9808bf147d55aa2930352af2e8c0

View closure receipt on GitHub →

Tweet thread template

tweet 1 of 8157 / 280

Two frontier models reviewed PR #25 on 6f7fc663. Both found this bug: critical security: Missing authentication/authorization on secret-management endpoints

tweet 2 of 8127 / 280

The vulnerable code (dashboard/app/api/secrets/route.ts:95-145): (full snippet at https://www.antfleet.dev/anatomy/6017bf3f-0)

tweet 3 of 8280 / 280

What Opus saw: "Both route handlers expose the ability to read which secrets are set, create/overwrite arbitrary GitHub Actions secrets (including ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN), and delete them. There is no authentication, no session check, no CSRF token, and no …

tweet 4 of 8280 / 280

What GPT-5 saw: "In POST /api/secrets, the secret value is supplied using '-b', which puts the secret on the process command line (argv). On many systems, process arguments are visible to other users and may be logged, leaking the secret. The auth route correctly uses stdin to …

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/6017bf3f-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/6017bf3f-0

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