Configuration & Environment
An MCP proxy's `/authorize` or `/consent` route returns HTML without `Content-Security-Policy: frame-ancestors` or `X-Frame-Options: DENY` — letting an attacker iframe the consent screen and trick users into approving via UI overlay.
Clickjacking embeds a target page in an iframe under attacker control, then overlays invisible UI to capture the user's click on the target's actual button. For OAuth consent, the click means "approve." The defense is a CSP `frame-ancestors 'none'` directive (preferred) or `X-Frame-Options: DENY` header — both are MUSTs in the MCP security best practices.
OAuth consent pages are the highest-value clickjacking target on an MCP server. The user only sees them rarely, so an unfamiliar UI wouldn't raise alarms. A clickjack on the consent page is an authorize-as-the-user attack — the attacker's malicious MCP client gets approved without the user even noticing.
@app.get("/authorize") |
def authorize(client_id: str) -> HTMLResponse: |
return HTMLResponse( |
f"<form action='/consent' method='POST'>Allow {client_id}?</form>" |
) |
@app.get("/authorize") |
def authorize(client_id: str) -> HTMLResponse: |
return HTMLResponse( |
f"<form action='/consent' method='POST'>Allow {client_id}?</form>", |
headers={ |
"Content-Security-Policy": "frame-ancestors 'none'", |
"X-Frame-Options": "DENY", |
}, |
) |
MCPSafe flags MCP-server files that have a `/authorize` or `/consent` route handler returning HTML (`HTMLResponse`, `render_template`, `templates.TemplateResponse`, `res.render`, `res.send("<html")`, `res.sendFile`, raw HTML string return, etc.) when no clickjacking protection identifier appears anywhere in the file (`frame-ancestors`, `X-Frame-Options: DENY|SAMEORIGIN`, `helmet()`, `Talisman()`, `from secure import`, etc.).
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now