Security in JavaScript
Security is a critical aspect of JavaScript development, especially for browser-based applications where code runs in an untrusted environment. A single vulnerability can expose user data, compromise accounts, or damage your organization’s reputation.
Threat Model Overview
| Threat | Attack Vector | Primary Defense |
|---|---|---|
| XSS | Inject malicious scripts via user input | Output encoding, CSP |
| CSRF | Trick browser into unwanted requests | SameSite cookies, tokens |
| Injection | Untrusted data executed as code | Parameterized queries, validation |
| Data exposure | Secrets in client-side code | Server-side storage, env vars |
| Supply chain | Compromised npm packages | Audits, lockfiles, pinning |
Preventing Cross-Site Scripting (XSS)
XSS occurs when attackers inject scripts that execute in other users’ browsers.
Types of XSS
- Stored XSS — malicious script saved in database, served to all viewers
- Reflected XSS — script in URL parameter reflected in page response
- DOM-based XSS — client-side JavaScript writes untrusted data to DOM unsafely
Output Encoding
Never insert user input into HTML without encoding:
function escapeHtml(text) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return String(text).replace(/[&<>"']/g, char => map[char]);
}
// BAD
element.innerHTML = userInput;
// GOOD
element.textContent = userInput;
// Or with encoding
element.innerHTML = escapeHtml(userInput);
Prefer textContent over innerHTML. Use DOMPurify if HTML rendering is required.
Content Security Policy (CSP)
CSP restricts which scripts, styles, and resources can load:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
Server header (preferred):
Content-Security-Policy: default-src 'self'; script-src 'self'
CSP blocks inline scripts and unauthorized external scripts even if XSS injection succeeds.
Preventing Cross-Site Request Forgery (CSRF)
CSRF tricks authenticated users into submitting unwanted requests (e.g., transferring money).
Defenses
<!-- Anti-CSRF token in forms -->
<form method="post" action="/transfer">
<input type="hidden" name="csrf_token" value="server-generated-token">
<input type="number" name="amount">
<button type="submit">Transfer</button>
</form>
// Include token in fetch requests
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ amount: 100 })
});
- Set cookies with
SameSite=StrictorSameSite=Lax - Verify
OriginandRefererheaders server-side - Use custom headers (
X-Requested-With) that simple form submissions cannot set
Avoiding Injection Attacks
Never concatenate user input into queries or shell commands:
// BAD — SQL injection risk (server-side)
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
// GOOD — parameterized query
db.query('SELECT * FROM users WHERE name = ?', [userInput]);
Validate and sanitize input at system boundaries:
function sanitizeUsername(input) {
const cleaned = input.trim();
if (!/^[a-zA-Z0-9_]{3,20}$/.test(cleaned)) {
throw new Error('Invalid username format');
}
return cleaned;
}
Secure Authentication
Never Store Secrets Client-Side
// BAD — visible to anyone who opens DevTools
const API_KEY = 'sk-live-abc123secret';
// GOOD — proxy through your server
const response = await fetch('/api/data'); // server adds API key
Token Storage
| Storage | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
| localStorage | High — JS can read | None | Avoid for auth tokens |
| HttpOnly cookie | Low — JS cannot read | Mitigate with SameSite | Preferred for sessions |
| Memory (variable) | Lost on refresh | None | OK for short-lived SPA tokens |
Use server-side libraries (bcrypt, argon2) for password hashing — never roll your own crypto.
Dependency Security
# Audit npm dependencies
npm audit
# Fix auto-fixable issues
npm audit fix
# Check for known vulnerabilities in CI
npx audit-ci --moderate
Best practices:
- Commit
package-lock.jsonfor reproducible installs - Review new dependencies before adding
- Use
npm ciin CI/CD (notnpm install) - Enable Dependabot or Snyk for automated alerts
Handling Errors Securely
Don’t expose stack traces or internal details to users:
// BAD
catch (error) {
res.status(500).json({ error: error.stack });
}
// GOOD
catch (error) {
logger.error('Payment failed', { userId, error: error.message });
res.status(500).json({ error: 'An unexpected error occurred' });
}
Additional Hardening
- HTTPS everywhere — enforce TLS; set
Secureflag on cookies - Subresource Integrity (SRI) — verify CDN scripts haven’t been tampered with
- Rate limiting — prevent brute force on login endpoints
- Input length limits — prevent denial-of-service via oversized payloads
- Principle of least privilege — RBAC on both client and server
Troubleshooting Security Issues
| Symptom | Investigation | Fix |
|---|---|---|
| Script runs from user comment | Check innerHTML usage | Use textContent or DOMPurify |
| Session hijacked | Check cookie flags | Add HttpOnly, Secure, SameSite |
| npm audit warnings | Run npm audit |
Update or replace vulnerable packages |
| CORS errors exposing data | Review Access-Control headers | Restrict allowed origins |
| Token visible in DevTools | Stored in localStorage | Move to HttpOnly cookie |
Security is not a feature you add at the end — it must be designed into every layer of your JavaScript application from authentication and input handling to dependency management and error responses.