Clickjacking and frame defences
Framing attacks against sensitive UI actions, and how frame-ancestors plus confirmation flows protect frontend apps.
Clickjacking puts your UI in a transparent iframe and tricks the user into clicking your buttons while they think they are clicking something else. The session is real. The intent is not.
Any frontend that performs sensitive actions with a single click is a candidate: delete, transfer, connect bank, change email.
Defence in the browser response
Frame denial belongs in HTTP headers. Content-Security-Policy frame-ancestors is the modern control. X-Frame-Options remains useful as defence in depth.
In Next.js middleware you must call NextResponse.next() so the request still reaches the app. Returning a bare Response with only headers replaces the page with an empty body.
- Always use NextResponse.next() (or rewrite/redirect) in middleware. Do not return an empty Response for normal pages.
- frame-ancestors 'none' for apps that should never be embedded.
- Allow only trusted parents if you genuinely need embedding.
- Do not rely on frame-busting JavaScript alone.
import { NextResponse } from "next/server";
export function middleware() {
const response = NextResponse.next();
response.headers.set(
"Content-Security-Policy",
"frame-ancestors 'none'"
);
response.headers.set("X-Frame-Options", "DENY");
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Content-Security-Policy",
value: "frame-ancestors 'none'",
},
{ key: "X-Frame-Options", value: "DENY" },
],
},
];
}
export function DeleteAccountButton({
onConfirm,
}: {
onConfirm: () => void;
}) {
return (
<button
type="button"
onClick={() => {
const ok = window.confirm("Delete account permanently?");
if (ok) onConfirm();
}}>
Delete account
</button>
);
}