fix(console): validate auth redirects (#45027)

This commit is contained in:
Adam 2026-08-26 05:41:23 -05:00 committed by GitHub
parent 1cc53890dc
commit ba4d0ea8bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 52 additions and 1 deletions

View file

@ -235,6 +235,7 @@
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"openai": "5.11.0",

View file

@ -11,6 +11,7 @@
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:",

View file

@ -0,0 +1,18 @@
import { describe, expect, test } from "bun:test"
import { isAllowedAuthorizationRedirect } from "./auth-redirect"
describe("authorization redirect validation", () => {
test("allows registered OpenCode callbacks", () => {
expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai/auth/callback")).toBe(true)
expect(isAllowedAuthorizationRedirect("app", "https://dev.opencode.ai/auth/callback")).toBe(true)
expect(isAllowedAuthorizationRedirect("app", "http://localhost:3000/auth/callback")).toBe(true)
expect(isAllowedAuthorizationRedirect("app", "http://127.0.0.1:3000/auth/callback")).toBe(true)
})
test("rejects unregistered clients and external redirects", () => {
expect(isAllowedAuthorizationRedirect("other", "https://opencode.ai/auth/callback")).toBe(false)
expect(isAllowedAuthorizationRedirect("app", "https://evil.example/callback")).toBe(false)
expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai.evil.example/callback")).toBe(false)
expect(isAllowedAuthorizationRedirect("app", "javascript:alert(1)")).toBe(false)
})
})

View file

@ -0,0 +1,18 @@
export const isAllowedAuthorizationRedirect = (clientID: string, redirectURI: string) => {
if (clientID !== "app") return false
const redirect = (() => {
try {
return new URL(redirectURI)
} catch {
return undefined
}
})()
if (redirect === undefined) return false
if (redirect.hostname === "localhost" || redirect.hostname === "127.0.0.1") {
return redirect.protocol === "http:" || redirect.protocol === "https:"
}
return (
redirect.protocol === "https:" &&
(redirect.hostname === "opencode.ai" || redirect.hostname.endsWith(".opencode.ai"))
)
}

View file

@ -17,6 +17,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { isAllowedAuthorizationRedirect } from "./auth-redirect.js"
type Env = {
AuthStorage: KVNamespace
@ -41,6 +42,17 @@ const MY_THEME: Theme = {
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const requestURL = new URL(request.url)
if (requestURL.pathname === "/authorize") {
const redirectURI = requestURL.searchParams.get("redirect_uri")
if (
redirectURI !== null &&
!isAllowedAuthorizationRedirect(requestURL.searchParams.get("client_id") ?? "", redirectURI)
) {
return new Response("Unauthorized client", { status: 400 })
}
}
const result = await issuer({
theme: MY_THEME,
providers: {
@ -102,6 +114,7 @@ export default {
namespace: env.AuthStorage,
}),
subjects,
allow: ({ clientID, redirectURI }) => Promise.resolve(isAllowedAuthorizationRedirect(clientID, redirectURI)),
async success(ctx, response) {
console.log(response)

View file

@ -6,6 +6,6 @@
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "react",
"types": ["@cloudflare/workers-types", "node"]
"types": ["@cloudflare/workers-types", "bun", "node"]
}
}