Merge 0a0c9c3264 into ae614d919f
commit
385868ae05
|
|
@ -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, """).replace(/'/g, "'")
|
||||
}
|
||||
|
||||
const HTML_SUCCESS = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -41,7 +45,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>`
|
||||
|
|
|
|||
|
|
@ -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, """).replace(/'/g, "'")
|
||||
}
|
||||
|
||||
const HTML_ERROR = (error: string) => `<div class="error">${escapeHtml(error)}</div>`
|
||||
|
||||
describe("CWE-79: XSS in oauth-callback.ts HTML_ERROR", () => {
|
||||
test("should escape script tags in error message", () => {
|
||||
const result = HTML_ERROR('<script>alert("xss")</script>')
|
||||
expect(result).not.toContain("<script>")
|
||||
expect(result).toContain("<script>")
|
||||
})
|
||||
|
||||
test("should escape img onerror payload", () => {
|
||||
const result = HTML_ERROR('<img src=x onerror=alert(1)>')
|
||||
expect(result).not.toContain("<img")
|
||||
expect(result).toContain("<img")
|
||||
})
|
||||
|
||||
test("should escape HTML entities in error_description", () => {
|
||||
const result = HTML_ERROR('access_denied&error_description=<b>bold</b>')
|
||||
expect(result).not.toContain("<b>")
|
||||
expect(result).toContain("<b>")
|
||||
})
|
||||
|
||||
test("should escape quotes to prevent attribute injection", () => {
|
||||
const result = HTML_ERROR('" onmouseover="alert(1)')
|
||||
expect(result).toContain(""")
|
||||
expect(result).not.toContain(' onmouseover="alert')
|
||||
})
|
||||
|
||||
test("should render normal error messages correctly", () => {
|
||||
const result = HTML_ERROR("access_denied")
|
||||
expect(result).toContain("access_denied")
|
||||
expect(result).toContain('class="error"')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue