Clickjacking (UI redress attack) is a malicious technique where attackers trick users into clicking something different from what they perceive. Here’s how to secure your React applications against clickjacking attacks:
Core Defense Mechanisms
1. Frame-Busting Headers
// In your server configuration (Express.js example)
app.use(helmet.frameguard({ action: 'deny' }));
// Equivalent to setting:
// X-Frame-Options: DENY
2. Content Security Policy (CSP)
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none';">
3. Client-Side Protection
// Frame-busting script (additional layer of defense)
useEffect(() => {
if (window !== window.top) {
window.top.location = window.location;
}
}, []);
Implementation Strategies
1. Server-Side Headers (Best Practice)
# Nginx configuration
add_header X-Frame-Options "DENY";
add_header Content-Security-Policy "frame-ancestors 'none'";
2. React-Specific Solutions
Protected Routes Component
const FrameGuard = ({ children }) => {
useEffect(() => {
if (window.self !== window.top) {
document.body.style.display = 'none';
window.top.location = window.self.location;
}
return () => {
document.body.style.display = 'block';
};
}, []);
return <>{children}</>;
};
// Usage
<FrameGuard>
<ProtectedRoute />
</FrameGuard>
Conditional Rendering
function App() {
const [isFramed, setIsFramed] = useState(false);
useEffect(() => {
setIsFramed(window.self !== window.top);
}, []);
return isFramed ? (
<div className="clickjacking-warning">
<h1>Security Alert</h1>
<p>This page cannot be displayed in a frame.</p>
<button onClick={() => (window.location.href = window.top.location.href)}>
Continue to site
</button>
</div>
) : (
<MainApp />
);
}
Advanced Protection Techniques
1. Dynamic CSP for Development
// webpack.config.js (for create-react-app eject)
const isProduction = process.env.NODE_ENV === 'production';
new HtmlWebpackPlugin({
meta: {
'Content-Security-Policy': {
'http-equiv': 'Content-Security-Policy',
'content': isProduction
? "frame-ancestors 'none';"
: "frame-ancestors 'self';"
}
}
});
2. Session-Based Frame Protection
// Server middleware
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', req.session?.allowedFraming ? 'ALLOW-FROM trusted.com' : 'DENY');
next();
});
3. Visual Protection for Sensitive Actions
const SecureButton = ({ onClick, children }) => {
const [requiresConfirmation, setRequiresConfirmation] = useState(false);
const handleClick = (e) => {
if (window.self !== window.top) {
e.preventDefault();
setRequiresConfirmation(true);
return;
}
onClick(e);
};
return (
<>
<button onClick={handleClick}>{children}</button>
{requiresConfirmation && (
<div className="secure-confirm-overlay">
<div className="secure-confirm-box">
<p>Security confirmation required</p>
<button onClick={() => setRequiresConfirmation(false)}>
Cancel
</button>
<button onClick={() => {
window.top.location = window.self.location;
}}>
Continue
</button>
</div>
</div>
)}
</>
);
};
Testing Your Defenses
1. Manual Testing
<!-- Create a test.html file -->
<iframe src="http://your-react-app.com"></iframe>
2. Automated Security Headers Check
// Jest test example
test('should have clickjacking protection headers', async () => {
const response = await fetch('http://localhost:3000');
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
expect(response.headers.get('Content-Security-Policy')).toContain("frame-ancestors 'none'");
});
3. Browser Extension Tests
- Use “XSS Me” or “Security Headers” browser extensions
- Check your site’s headers at securityheaders.com
Special Cases and Workarounds
1. Allowing Specific Iframe Embeds
// For trusted domains only
app.use(helmet.frameguard({
action: 'allow-from',
domain: 'https://trusted-platform.com'
}));
// CSP equivalent
<meta http-equiv="Content-Security-Policy" content="frame-ancestors https://trusted-platform.com">
2. Legacy Browser Support
// Combine modern and legacy techniques
const ClickjackProtection = () => {
useEffect(() => {
// Modern browsers
try {
if (window.self !== window.top) {
window.top.location = window.self.location;
}
} catch (e) {
// Legacy browsers fallback
document.body.innerHTML = '';
document.write('This page cannot be framed.');
}
// Additional style protection
document.body.style.display = 'none';
if (window.self === window.top) {
document.body.style.display = 'block';
}
}, []);
return null;
};
Best Practices Checklist
- [ ] Implement
X-Frame-Options: DENY
header - [ ] Set CSP
frame-ancestors 'none'
directive - [ ] Add client-side frame-busting as secondary defense
- [ ] Protect sensitive actions with visual confirmation
- [ ] Test with iframe embedding attempts
- [ ] Monitor security headers regularly
- [ ] Educate team about clickjacking risks
- [ ] Document framing policies for your application
Ongoing Maintenance
- Header Monitoring
curl -I https://yourapp.com | grep -iE 'x-frame-options|csp'
- Automated Security Scanning
# GitHub Actions example
- name: Security scan
uses: OWASP/glue@v1
with:
target: http://localhost:3000
scanner: arachni
- Policy Review
- Quarterly review of framing requirements
- Update allowed domains list as needed
- Remove legacy exceptions when possible