Quiz: Spot the Vulnerability
Think Like an Attacker
Security bugs hide in code that looks completely normal. The function works, the tests pass, the feature ships. Then someone crafts a URL, sends a message, or pollutes a prototype — and your app is compromised.
This quiz trains the one skill that separates secure engineers from everyone else: reading code with adversarial eyes. For every snippet below, assume an attacker is watching and ask yourself: what would they do with this?
Every piece of user-controlled data is a loaded weapon. URLs, query params, form inputs, cookies, postMessage events, imported JSON — treat them all as hostile until proven safe. The security mindset is simple: never trust, always validate, minimize blast radius. Before you write any code that touches external data, ask three questions: Where does this data come from? What is the worst thing someone could put in it? What happens if they do?
Question 1: The Rendering Shortcut
function renderUserBio(bio) {
const container = document.getElementById('bio');
container.innerHTML = bio;
}
// Called with data from the server:
renderUserBio(userData.bio);
Question 2: The Open Door
// server.js
app.use(cors({
origin: true,
credentials: true,
}));
Question 3: The Convenient Storage
// After successful login
function handleLogin(response) {
const { token } = response.data;
localStorage.setItem('auth_token', token);
}
// On every API call
function getAuthHeader() {
return {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
};
}
Question 4: The Missing Header
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Banking App</title>
<link rel="stylesheet" href="/styles.css" />
</head>
Question 5: The Innocent Merge
function deepMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Merging user preferences from an API response
const config = deepMerge(defaults, userPreferences);
Question 6: The Forgetful Cookie
// Express.js session setup
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
maxAge: 24 * 60 * 60 * 1000,
},
}));
Question 7: The Trusting Listener
// In your app's iframe communication handler
window.addEventListener('message', (event) => {
const { action, payload } = event.data;
if (action === 'updateProfile') {
updateUserProfile(payload);
}
if (action === 'deleteAccount') {
deleteUserAccount();
}
});
Question 8: The Dynamic Evaluator
function calculateUserFormula(formula) {
return eval(formula);
}
// Used in a spreadsheet-like feature:
const result = calculateUserFormula(cellInput.value);
Question 9: The Reflected Link
// React component
function SearchResults() {
const params = new URLSearchParams(window.location.search);
const query = params.get('q') || '';
return (
<div>
<h1>Results for: {query}</h1>
<p dangerouslySetInnerHTML={{ __html: highlightMatches(query) }} />
</div>
);
}
Question 10: The Leaky Redirect
function handleOAuthCallback() {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
const redirect = params.get('redirect') || '/dashboard';
if (token) {
sessionStorage.setItem('token', token);
window.location.href = redirect;
}
}
Question 11: The Shared Secret
// config.js — bundled with the frontend
const config = {
apiUrl: 'https://api.myapp.com',
apiKey: 'sk_live_a1b2c3d4e5f6',
stripePublishableKey: 'pk_live_x9y8z7',
databaseUrl: 'postgres://admin:password@db.myapp.com:5432/prod',
};
export default config;
Question 12: The Template Builder
function renderTemplate(template, data) {
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] || '';
});
}
// User creates custom email templates
const html = renderTemplate(userTemplate, {
name: userName,
email: userEmail,
});
document.getElementById('preview').innerHTML = html;
Scoring Guide
| Score | Assessment |
|---|---|
| 11-12 | You think like an attacker. Exceptional security instincts. |
| 8-10 | Strong foundations. Review the ones you missed — each gap is a real attack vector. |
| 5-7 | You know the common ones but miss subtler issues. Study OWASP Top 10 for frontend. |
| 0-4 | Security needs focused attention. Start with XSS and CSRF — they account for most real-world frontend attacks. |
The Security Checklist Mindset
- 1Never trust user input — validate and sanitize at every boundary, even data from your own server
- 2Never use innerHTML, eval, or dangerouslySetInnerHTML with unsanitized data
- 3Store secrets on the server only — anything in the client bundle is public
- 4Always validate event.origin in postMessage handlers and redirect URLs
- 5Set all cookie security attributes: HttpOnly, Secure, SameSite, and appropriate expiration
- 6Deploy a strict Content Security Policy as your last line of defense against XSS
- 7Guard deep merge and object spread operations against prototype pollution
- 8Whitelist CORS origins explicitly — never reflect arbitrary origins with credentials
| What developers do | What they should do |
|---|---|
| Using innerHTML to render dynamic content because it is simpler than DOM APIs innerHTML parses and executes embedded scripts and event handlers, enabling XSS | Use textContent for plain text, or sanitize with DOMPurify before innerHTML |
| Storing JWTs in localStorage because cookies feel old-fashioned localStorage is readable by any script on the page — one XSS and the token is stolen | Use HttpOnly Secure SameSite cookies for authentication tokens |
| Setting CORS origin to true or * with credentials for convenience during development Any website can make authenticated requests to your API and read the response | Whitelist specific trusted origins and never ship wildcard credentials |
| Trusting postMessage events without checking origin Any window with a reference to yours can send messages with arbitrary data | Always validate event.origin against a whitelist before processing the message |
| Putting API secret keys in frontend config files or environment variables without the server-only prefix The entire client bundle is visible in browser DevTools — secrets are not secret | Keep secrets in server-only env vars and never import them in client code |