Clickjacking in Express.js Applications
Express.js does not set X-Frame-Options by default, so any page served by your Express app can be embedded in an iframe on a malicious site. Helmet's frameguard middleware adds this header, but Express apps without Helmet — or with Helmet misconfigured — remain vulnerable to clickjacking attacks that trick users into performing unintended actions.
Scan Your Express AppHow Clickjacking Manifests in Express
Express applications that serve HTML pages without X-Frame-Options or CSP frame-ancestors are vulnerable to clickjacking. This includes server-rendered pages (EJS, Pug, Handlebars), single-page applications served from Express static middleware, and admin panels. The vulnerability is most dangerous on pages with state-changing actions: account deletion, password changes, payment confirmations, and admin operations. The attacker overlays a transparent iframe containing your Express-served page on top of a decoy page, positioning the dangerous button under the user's cursor. Express apps that use Helmet but override frameguard can also be vulnerable. A common mistake is helmet({ frameguard: false }) to allow embedding during development, then deploying that configuration to production. API-only Express applications that also serve a Swagger UI, admin panel, or static documentation page may only apply Helmet to API routes, leaving the HTML-serving routes unprotected.
Real-World Impact
An Express application served an admin panel at /admin. The panel had a "Delete User" button that used a POST request with the user's session cookie. An attacker created a page with an invisible iframe loading /admin/users/123/delete, positioning the submit button under a "Download Free Report" link. When an admin clicked the link, they unknowingly deleted a user account. A financial application built with Express served a transfer confirmation page. An attacker embedded it in an iframe, pre-filled the transfer amount and recipient via URL parameters, and positioned the "Confirm" button under an innocuous link on their page.
Step-by-Step Fix
Enable Helmet frameguard
Helmet's frameguard sets X-Frame-Options on all responses. Enable it as part of your standard Helmet configuration.
import express from 'express';
import helmet from 'helmet';
const app = express();
// Helmet sets X-Frame-Options: SAMEORIGIN by default
// Override to DENY for stricter protection
app.use(helmet({
frameguard: { action: 'deny' },
}));
// Helmet also sets frameguard with its defaults:
// app.use(helmet()); // Sets X-Frame-Options: SAMEORIGINAdd CSP frame-ancestors alongside X-Frame-Options
frame-ancestors is the modern replacement for X-Frame-Options and supports domain allowlists.
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
frameAncestors: ["'none'"], // Blocks all framing
},
},
frameguard: { action: 'deny' }, // Fallback for older browsers
}));Allow framing for specific embeddable routes
If certain routes (widgets, embeds) must be framed, apply different headers to those routes only.
import express from 'express';
import helmet from 'helmet';
const app = express();
// Default: block all framing
app.use(helmet({ frameguard: { action: 'deny' } }));
// Override for embeddable routes
app.use('/embed', (req, res, next) => {
// Remove the DENY header set by Helmet
res.removeHeader('X-Frame-Options');
// Set permissive frame-ancestors for trusted domains only
res.setHeader(
'Content-Security-Policy',
"frame-ancestors 'self' https://trusted-partner.com"
);
next();
});
app.get('/embed/widget', (req, res) => {
res.render('widget'); // Can be framed by trusted-partner.com
});Prevention Best Practices
1. Use Helmet's frameguard middleware to set X-Frame-Options: DENY. 2. Add CSP frame-ancestors via Helmet's contentSecurityPolicy or custom middleware. 3. Never disable frameguard in production, even temporarily. 4. Ensure frame protection applies to all routes, including admin panels and static pages. 5. For embeddable widgets, use a separate Express route with restricted frame-ancestors.
How to Test
1. Create an HTML file with <iframe src="http://localhost:3000"></iframe> and open it. If your app loads, it is vulnerable. 2. Run curl -I http://localhost:3000/ and look for X-Frame-Options or frame-ancestors in the response. 3. Check all HTML-serving routes, not just the main page — admin panels and static pages are often overlooked. 4. Verify Helmet is applied before route handlers in the middleware chain. 5. Use Vibe App Scanner to automatically detect missing clickjacking protection in your Express application.
Frequently Asked Questions
Does Helmet prevent clickjacking by default?
Yes. Helmet's default configuration includes frameguard which sets X-Frame-Options: SAMEORIGIN. This allows your own domain to frame the page but blocks other domains. For stricter protection, set frameguard: { action: 'deny' } to block all framing including from your own domain.
Does clickjacking affect Express API-only applications?
Clickjacking targets HTML pages rendered in the browser, not JSON API responses. However, if your Express app serves any HTML (admin panel, documentation, error pages, Swagger UI), those pages are vulnerable. Set frame protection headers on all responses, not just API routes.
Can JavaScript-based frame busting replace X-Frame-Options?
No. JavaScript frame-busting code (if (top !== self) top.location = self.location) can be bypassed using the sandbox attribute on the iframe, which disables scripts. X-Frame-Options and CSP frame-ancestors are enforced by the browser before any JavaScript runs, making them reliable. Never rely on JavaScript for clickjacking protection.
Related Security Resources
Is Your Express App Vulnerable to Clickjacking?
VAS automatically scans for clickjacking vulnerabilities in Express applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Express stack.