chore(opencode): consolidate escape logic (#32360)

This commit is contained in:
Aiden Cline
2026-06-14 21:27:02 -04:00
committed by GitHub
parent a9a4b2f00f
commit a774c62eac
7 changed files with 44 additions and 51 deletions
+1 -9
View File
@@ -1,5 +1,6 @@
import { createConnection } from "net"
import { createServer } from "http"
import { escapeHtml } from "@/util/html"
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
// Current callback server configuration (may differ from defaults if custom redirectUri is used)
@@ -26,15 +27,6 @@ const HTML_SUCCESS = `<!DOCTYPE html>
</body>
</html>`
function escapeHtml(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
}
const HTML_ERROR = (error: string) => `<!DOCTYPE html>
<html>
<head>
+10 -9
View File
@@ -5,6 +5,7 @@ import os from "os"
import { setTimeout as sleep } from "node:timers/promises"
import { createServer } from "http"
import { OpenAIWebSocketPool } from "./ws-pool"
import { escapeHtml } from "@/util/html"
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
const ISSUER = "https://auth.openai.com"
@@ -178,7 +179,7 @@ const HTML_SUCCESS = `<!doctype html>
</body>
</html>`
const HTML_ERROR = (error: string) => `<!doctype html>
export const renderOAuthError = (error: string) => `<!doctype html>
<html>
<head>
<title>OpenCode - Codex Authorization Failed</title>
@@ -221,7 +222,7 @@ const HTML_ERROR = (error: string) => `<!doctype html>
<div class="container">
<h1>Authorization Failed</h1>
<p>An error occurred during authorization.</p>
<div class="error">${error}</div>
<div class="error">${escapeHtml(error)}</div>
</div>
</body>
</html>`
@@ -254,8 +255,8 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
const errorMsg = errorDescription || error
pendingOAuth?.reject(new Error(errorMsg))
pendingOAuth = undefined
res.writeHead(200, { "Content-Type": "text/html" })
res.end(HTML_ERROR(errorMsg))
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
res.end(renderOAuthError(errorMsg))
return
}
@@ -263,8 +264,8 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
const errorMsg = "Missing authorization code"
pendingOAuth?.reject(new Error(errorMsg))
pendingOAuth = undefined
res.writeHead(400, { "Content-Type": "text/html" })
res.end(HTML_ERROR(errorMsg))
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" })
res.end(renderOAuthError(errorMsg))
return
}
@@ -272,8 +273,8 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
const errorMsg = "Invalid state - potential CSRF attack"
pendingOAuth?.reject(new Error(errorMsg))
pendingOAuth = undefined
res.writeHead(400, { "Content-Type": "text/html" })
res.end(HTML_ERROR(errorMsg))
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" })
res.end(renderOAuthError(errorMsg))
return
}
@@ -284,7 +285,7 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
.then((tokens) => current.resolve(tokens))
.catch((err) => current.reject(err))
res.writeHead(200, { "Content-Type": "text/html" })
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
res.end(HTML_SUCCESS)
return
}
+1 -19
View File
@@ -2,6 +2,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { OAUTH_DUMMY_KEY } from "../auth"
import { createServer } from "http"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { escapeHtml } from "@/util/html"
// Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
// non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships
@@ -74,25 +75,6 @@ function generateState(): string {
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
}
export function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (char) => {
switch (char) {
case "&":
return "&amp;"
case "<":
return "&lt;"
case ">":
return "&gt;"
case '"':
return "&quot;"
case "'":
return "&#39;"
default:
return char
}
})
}
interface TokenResponse {
access_token: string
refresh_token: string
+8
View File
@@ -0,0 +1,8 @@
export function escapeHtml(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
}
@@ -4,6 +4,7 @@ import {
parseJwtClaims,
extractAccountIdFromClaims,
extractAccountId,
renderOAuthError,
type IdTokenClaims,
} from "../../src/plugin/openai/codex"
@@ -14,6 +15,14 @@ function createTestJwt(payload: object): string {
}
describe("plugin.codex", () => {
test("escapes provider errors in callback HTML", () => {
const error = `</div><script>alert("xss" & 'more')</script>`
const html = renderOAuthError(error)
expect(html).toContain("&lt;/div&gt;&lt;script&gt;alert(&quot;xss&quot; &amp; &#39;more&#39;)&lt;/script&gt;")
expect(html).not.toContain(error)
})
describe("parseJwtClaims", () => {
test("parses valid JWT with claims", () => {
const payload = { email: "test@example.com", chatgpt_account_id: "acc-123" }
-14
View File
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
import {
accessTokenIsExpiring,
buildAuthorizeUrl,
escapeHtml,
pollDeviceCodeToken,
requestDeviceCode,
XaiAuthPlugin,
@@ -103,19 +102,6 @@ describe("plugin.xai", () => {
})
})
describe("escapeHtml", () => {
test("escapes HTML metacharacters", () => {
expect(escapeHtml(`</div><script>alert(1)</script><div class="x">`)).toBe(
"&lt;/div&gt;&lt;script&gt;alert(1)&lt;/script&gt;&lt;div class=&quot;x&quot;&gt;",
)
expect(escapeHtml("a & b")).toBe("a &amp; b")
expect(escapeHtml("it's fine")).toBe("it&#39;s fine")
expect(escapeHtml("invalid_grant")).toBe("invalid_grant")
expect(escapeHtml("")).toBe("")
expect(escapeHtml("&<")).toBe("&amp;&lt;")
})
})
describe("loader", () => {
test("returns no options unless stored auth is OAuth and exposes methods in order", async () => {
const hooks = await XaiAuthPlugin({} as any)
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, test } from "bun:test"
import { escapeHtml } from "../../src/util/html"
describe("escapeHtml", () => {
test("escapes HTML metacharacters", () => {
expect(escapeHtml(`</div><script>alert(1)</script><div class="x">`)).toBe(
"&lt;/div&gt;&lt;script&gt;alert(1)&lt;/script&gt;&lt;div class=&quot;x&quot;&gt;",
)
expect(escapeHtml("a & b")).toBe("a &amp; b")
expect(escapeHtml("it's fine")).toBe("it&#39;s fine")
expect(escapeHtml("invalid_grant")).toBe("invalid_grant")
expect(escapeHtml("")).toBe("")
expect(escapeHtml("&<")).toBe("&amp;&lt;")
})
})