diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts
index dd1d886fc1..0be49033e7 100644
--- a/packages/opencode/src/mcp/oauth-callback.ts
+++ b/packages/opencode/src/mcp/oauth-callback.ts
@@ -5,6 +5,10 @@ import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
const log = Log.create({ service: "mcp.oauth-callback" })
+function escapeHtml(str: string): string {
+ return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'")
+}
+
const HTML_SUCCESS = `
@@ -41,7 +45,7 @@ const HTML_ERROR = (error: string) => `
Authorization Failed
An error occurred during authorization.
-
${error}
+
${escapeHtml(error)}
`
diff --git a/packages/opencode/test/mcp/oauth-callback-xss.test.ts b/packages/opencode/test/mcp/oauth-callback-xss.test.ts
new file mode 100644
index 0000000000..89cd4c6742
--- /dev/null
+++ b/packages/opencode/test/mcp/oauth-callback-xss.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, test } from "bun:test"
+
+/**
+ * CWE-79: Cross-Site Scripting (XSS)
+ * File: packages/opencode/src/mcp/oauth-callback.ts
+ *
+ * HTML_ERROR interpolated error string directly into HTML template.
+ * Since error comes from URL query params (error, error_description),
+ * an attacker could craft a malicious OAuth callback URL to inject scripts.
+ */
+
+function escapeHtml(str: string): string {
+ return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'")
+}
+
+const HTML_ERROR = (error: string) => `${escapeHtml(error)}
`
+
+describe("CWE-79: XSS in oauth-callback.ts HTML_ERROR", () => {
+ test("should escape script tags in error message", () => {
+ const result = HTML_ERROR('')
+ expect(result).not.toContain("