Clickjacking with Bearer Tokens
How Clickjacking Manifests in Bearer Tokens
Clickjacking attacks exploit the fact that users can be tricked into clicking on seemingly innocuous UI elements that trigger unintended actions. When Bearer Tokens are involved, clickjacking becomes particularly dangerous because it can lead to unauthorized API calls using stolen or manipulated tokens.
The most common clickjacking scenario with Bearer Tokens occurs when an attacker embeds your application in an invisible iframe and overlays deceptive UI elements. A user might believe they're clicking a "Download PDF" button, but they're actually clicking a hidden "Delete Account" button that makes an API call using their Bearer Token.
Consider this vulnerable pattern:
<!-- Victim application -->
<button onclick="makeAPICall()">Download Report</button>
<script>
function makeAPICall() {
fetch('/api/reports/download', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
}
});
}
</script>An attacker could create a malicious page that loads this application in an iframe with zero opacity:
<!-- Malicious clickjacking page -->
<style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 2;
}
.fake-button {
position: absolute;
top: 200px;
left: 100px;
width: 150px;
height: 50px;
background: blue;
color: white;
z-index: 1;
}
</style>
<iframe src="https://victim-app.com"></iframe>
<div class="fake-button" onclick="clickThrough()">Click Here for Free Gift</div>
<script>
function clickThrough() {
// Coordinates aligned with victim app's button
const iframe = document.querySelector('iframe');
const clickX = 120;
const clickY = 220;
iframe.contentWindow.document.elementFromPoint(clickX, clickY).click();
}
</script>When the user clicks the "Free Gift" button, they're actually clicking the victim application's button through the iframe. The API call executes with the user's Bearer Token, performing actions they never intended.
Another variant involves cursorjacking, where the attacker manipulates the cursor position to mislead users about where they're actually clicking. This is particularly effective when combined with Bearer Tokens because the token is automatically included in the request headers.
Bearer Tokens-Specific Detection
Detecting clickjacking vulnerabilities in Bearer Token implementations requires both manual testing and automated scanning. Here's how to identify these issues:
Manual Testing Steps:
1. Test frame embedding:
curl -I https://your-api.com
# Look for X-Frame-Options header
2. Check CSP headers:
curl -I https://your-api.com | grep "Content-Security-Policy"
# Verify frame-ancestors directive
3. Test with iframe:
<iframe src="https://your-api.com"></iframe>
# Try to interact with elements
4. Verify same-origin policy:
# Attempt to access localStorage from different origin
Automated Scanning with middleBrick:
middleBrick's clickjacking detection specifically tests Bearer Token endpoints by:
- Checking for X-Frame-Options header presence and correct values (DENY, SAMEORIGIN)
- Analyzing Content-Security-Policy for frame-ancestors restrictions
- Testing whether sensitive endpoints can be embedded in iframes
- Verifying that state-changing operations require additional verification beyond just Bearer Token presence
- Checking for missing anti-CSRF tokens in conjunction with Bearer Token usage
Run middleBrick to scan your Bearer Token endpoints:
npx middlebrick scan https://api.yourservice.com/user/profile
# Or integrate into CI/CD
middlebrick scan --fail-below B --output jsonThe scan results will show clickjacking risk alongside other Bearer Token-specific vulnerabilities like BOLA (Broken Object Level Authorization) and missing rate limiting.
Bearer Tokens-Specific Remediation
Securing Bearer Token endpoints against clickjacking requires multiple defensive layers. Here are Bearer Tokens-specific implementations:
1. Frame-Busting JavaScript:
// Implement in all pages handling Bearer Token operations
if (self !== top) {
try {
top.location = self.location;
} catch (e) {
// Fallback for sandboxed iframes
document.body.style.display = 'none';
document.write('<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:red;color:white;text-align:center;padding-top:200px;font-size:24px">Security Alert: This page cannot be embedded</div>');
}
}
// Enhanced version with token validation
window.addEventListener('load', function() {
if (window.location !== window.parent.location) {
// Destroy any sensitive Bearer Token operations
const sensitiveElements = document.querySelectorAll('[data-sensitive="true"]');
sensitiveElements.forEach(el => el.remove());
// Clear localStorage for security-critical tokens
if (localStorage.getItem('authToken')) {
localStorage.removeItem('authToken');
console.warn('Bearer token cleared due to clickjacking attempt');
}
}
});2. HTTP Header Protection:
// Express.js middleware for Bearer Token endpoints
app.use('/api/*', (req, res, next) => {
// X-Frame-Options: DENY prevents all framing
res.setHeader('X-Frame-Options', 'DENY');
// Content-Security-Policy with frame-ancestors
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
// Additional CSP for modern browsers
res.setHeader('X-Content-Security-Policy', "allow 'none'");
next();
});
// For endpoints that must support framing from specific domains
app.use('/public/*', (req, res, next) => {
res.setHeader('X-Frame-Options', 'ALLOW-FROM https://trusted.com');
res.setHeader('Content-Security-Policy', "frame-ancestors https://trusted.com");
next();
});3. Double-Submit Cookie with Bearer Token:
// Generate anti-CSRF token and bind to Bearer Token session
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// Middleware to set CSRF cookie and validate
app.use((req, res, next) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
return next();
}
const csrfToken = req.cookies._csrf || generateCSRFToken();
res.cookie('_csrf', csrfToken, { httpOnly: true, secure: true });
// Store mapping between Bearer Token and CSRF token
const bearerToken = req.headers.authorization.substring(7);
storeTokenMapping(bearerToken, csrfToken);
next();
});
// Validate CSRF for state-changing operations
app.put('/api/profile', (req, res) => {
const bearerToken = req.headers.authorization.substring(7);
const csrfToken = req.cookies._csrf;
if (!validateTokenMapping(bearerToken, csrfToken)) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
// Process request
});4. Origin Verification:
// Verify request origin for Bearer Token operations
app.use((req, res, next) => {
const allowedOrigins = [
'https://yourdomain.com',
'https://app.yourdomain.com'
];
const origin = req.headers.origin || req.headers.referer;
if (origin && !allowedOrigins.includes(origin)) {
// For sensitive Bearer Token operations, reject requests from unexpected origins
if (req.path.startsWith('/api/sensitive')) {
return res.status(403).json({ error: 'Invalid origin' });
}
}
next();
});5. User Interaction Confirmation:
// Require additional confirmation for critical operations
function confirmCriticalAction(action, bearerToken) {
return new Promise((resolve, reject) => {
// Generate a one-time confirmation token
const confirmationToken = crypto.randomBytes(16).toString('hex');
// Store token temporarily (server-side)
storeConfirmationToken(bearerToken, confirmationToken, action);
// Show confirmation dialog (client-side)
const confirmed = window.confirm(`Confirm ${action}? This cannot be undone.`);
if (confirmed) {
resolve(confirmationToken);
} else {
reject(new Error('Action cancelled'));
}
});
}
// API endpoint using confirmation
app.delete('/api/user', async (req, res) => {
try {
const confirmationToken = await confirmCriticalAction('account deletion', req.headers.authorization.substring(7));
// Verify confirmation token server-side
if (!verifyConfirmationToken(confirmationToken)) {
return res.status(403).json({ error: 'Invalid confirmation' });
}
// Proceed with deletion
await deleteUser(req.user.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});