Skip to main content
Published
ClickjackingSecurityCSP

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.
middleware.tsts
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).*)"],
};
Attach frame defences, then continue the request.
next.config.mjs (alternative)js
async headers() {
  return [
    {
      source: "/:path*",
      headers: [
        {
          key: "Content-Security-Policy",
          value: "frame-ancestors 'none'",
        },
        { key: "X-Frame-Options", value: "DENY" },
      ],
    },
  ];
}
Static frame headers can also live in next.config when you do not need request logic.
components/SensitiveAction.tsxtsx
export function DeleteAccountButton({
  onConfirm,
}: {
  onConfirm: () => void;
}) {
  return (
    <button
      type="button"
      onClick={() => {
        const ok = window.confirm("Delete account permanently?");
        if (ok) onConfirm();
      }}>
      Delete account
    </button>
  );
}
UX defence: confirm destructive actions. Headers still come first.

More writing

Availability

Open to opportunities

Open to senior product engineer roles with real ownership across the stack. Security-minded by default.

UK-based · Remote / hybrid · Permanent or contract