Clickjacking in Strapi (Typescript)
Clickjacking in Strapi with Typescript — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side injection vulnerability where an attacker tricks a user into clicking on a hidden or disguised UI element inside an embedded frame (iframe). In Strapi, a common exposure path occurs when an admin or authenticated action page is embedded without appropriate frame-busting or CSP directives. Even though Strapi is a backend CMS, its admin React UI can be rendered inside an iframe on a malicious site, leading to unauthorized actions when an active admin session exists.
With Typescript, Strapi plugins and customizations often introduce new controller routes or modify admin views. If these routes are rendered inside an iframe and lack X-Frame-Options or Content-Security-Policy: frame-ancestors, the UI becomes susceptible. For example, a custom Typescript controller might expose an admin page at /admin/transfer-funds. When embedded on an attacker page, a crafted UI can overlay invisible buttons or links, causing an admin to unintentionally perform state-changing requests. Strapi’s admin panel is a frequent target because it often contains elevated privileges and sensitive operations. The risk increases when custom Typescript controllers or components do not explicitly opt out of framing, as the default Strapi admin may not enforce strict CSP in all deployments.
Moreover, during development, developers may expose endpoints via custom Typescript services without considering frame-ancestors policies. Since Strapi’s admin is a Single Page Application, routes can be deep-linked and embedded. Without proper CSP or frame-busting headers, an attacker can create a page that loads the Strapi admin inside a transparent iframe, overlaying interactive elements on top of critical buttons. This becomes especially dangerous when combined with social engineering, prompting an admin to click a seemingly benign element while performing an unintended action in Strapi’s UI.
Typescript-Specific Remediation in Strapi — concrete code fixes
Remediation focuses on enforcing frame-ancestors policy and ensuring custom Typescript controllers and views do not introduce unsafe embeddable surfaces. Strapi allows defining HTTP security headers via middleware, which is the recommended place to set CSP and X-Frame-Options for admin and custom endpoints.
Below is a Typescript example of Strapi middleware that sets strong CSP with frame-ancestors and X-Frame-Options. This ensures that even if a custom route is embedded, browsers will refuse to render it in an iframe unless explicitly allowed.
// src/middlewares/security/middleware.ts
import { MiddlewareInitialize } from '@strapi/strapi';
export default (): MiddlewareInitialize => {
return (config, { strapi }) => {
return async (ctx, next) => {
// Set X-Frame-Options for broad compatibility
ctx.response.set('X-Frame-Options', 'DENY');
// Content-Security-Policy: restrict frame embedding
ctx.response.set(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self';"
);
await next();
};
};
};
For admin-specific routes, ensure the middleware is applied globally or scoped to admin paths. You can also disable admin embedding explicitly for custom plugin views by setting headers in the controller response:
// src/api/custom-route/controllers/custom.ts
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::custom.custom', ({ strapi }) => ({
async find(ctx) {
const response = await super.find(ctx);
// Explicitly prevent framing for this endpoint
ctx.response.set('X-Frame-Options', 'DENY');
ctx.response.set(
'Content-Security-Policy',
"default-src 'none'; frame-ancestors 'none';"
);
return response;
},
}));
In the admin panel, avoid rendering iframes that point to internal Strapi routes. If embedding third-party content is necessary, use CSP frame-ancestors with specific origins instead of 'none'. Validate and sanitize any URL parameters that influence iframe sources to prevent open redirect or injection into embedded contexts.
Finally, include frame-busting JavaScript as a defense-in-depth measure for legacy browsers that may ignore CSP. Although not a replacement for headers, it provides additional protection during transitions:
// public/framebuster.js
if (window.self !== window.top) {
window.top.location = window.self.location;
}