Secure Coding for JavaScript Developers
JavaScript runs everywhere a browser does, which makes it one of the most attacked surfaces in software development. Most vulnerabilities don’t come from exotic exploits—they come from predictable patterns in everyday code. This guide covers the secure JavaScript coding practices that matter most when your code runs directly in the browser.
Key Takeaways
- DOM-based XSS is one of the most common JavaScript vulnerabilities—avoid passing untrusted data to sinks like
innerHTML,eval(), ordocument.write() - Use
textContentinstead ofinnerHTMLfor user-supplied data, and sanitize with DOMPurify when you genuinely need to render HTML - Never use
eval(),Function(), or string-basedsetTimeout/setIntervalwith dynamic input - Enforce Content Security Policy (CSP) with nonces or hashes and pair it with Trusted Types for defense in depth
- Avoid storing authentication tokens or secrets in
localStorageorsessionStorage—preferHttpOnlycookies for session identifiers - Always validate
event.originwhen handlingpostMessageevents - Audit your dependency tree regularly and use Subresource Integrity for CDN-loaded scripts
DOM-Based XSS: One of the Most Common JavaScript Vulnerabilities
Preventing XSS in JavaScript starts with understanding where it actually originates. DOM-based XSS happens when your code reads from an attacker-controlled source—like location.hash, document.referrer, or URLSearchParams—and writes it into the page unsafely.
The core rule: never pass untrusted data to a sink that interprets HTML or executes code.
Unsafe sinks to avoid with dynamic data:
// ❌ All of these can execute injected scripts
element.innerHTML = userInput
element.outerHTML = userInput
document.write(userInput)
eval(userInput)
setTimeout(userInput, 0) // string form only
new Function(userInput)()
Safe alternatives:
// ✅ Treats content as text, never as markup
element.textContent = userInput
element.innerText = userInput
When you genuinely need to render HTML—a rich text editor, for example—sanitize first with DOMPurify:
// ✅ Safe rich-text rendering
element.innerHTML = DOMPurify.sanitize(userInput)
The same logic applies to setAttribute. Dynamic attributes like href, src, and event handlers (onclick, onload) can execute JavaScript. Stick to static, non-executable attributes when working with user-supplied values.
Dynamic Code Execution Is Always Unsafe
eval(), Function(), and string-based setTimeout/setInterval are not just bad practice—they’re direct code injection vectors. There is no safe way to use them with untrusted input.
If you’re parsing data, use JSON.parse(). If you need dynamic behavior, use data structures and explicit logic rather than runtime code generation.
Content Security Policy as a Defense Layer
Content Security Policy (CSP) limits what scripts can run and where they can load from. A strict CSP using nonces or hashes—rather than 'unsafe-inline'—significantly reduces the blast radius of any XSS that slips through.
Pair CSP with Trusted Types in supported browsers to enforce safe DOM writes at the API level. Browser support for Trusted Types continues to expand and can be tracked on webstatus.dev.
Discover how at OpenReplay.com.
Secure Browser APIs: Cookies and Client-Side Storage
Cookies holding session identifiers should always carry HttpOnly (blocks JavaScript access), Secure (HTTPS only), and SameSite=Strict or Lax (helps mitigate CSRF). Setting these server-side is more reliable than doing it from JavaScript.
localStorage and sessionStorage are accessible to any script on the page. Avoid storing authentication tokens, session secrets, or sensitive user data there—an XSS vulnerability immediately exposes everything in storage.
Cross-Origin Messaging with postMessage
postMessage is useful but easy to misuse. Always validate the origin of incoming messages before acting on their data:
window.addEventListener('message', (event) => {
// ✅ Always validate origin before processing
if (event.origin !== 'https://trusted-origin.com') return
handleMessage(event.data)
})
When sending messages, avoid using '*' as the targetOrigin unless you truly have no fixed destination. On the receiving side, always validate event.origin to ensure the message came from a trusted site. More details about secure usage are covered in the postMessage documentation.
JavaScript Supply Chain Security
JavaScript supply chain security is a growing concern. A single compromised or malicious package can affect thousands of applications. Practical steps:
- Run
npm auditor use Snyk to catch known vulnerabilities in dependencies - Commit your lockfile (
package-lock.jsonoryarn.lock) and treat unexpected changes as a red flag - Use Subresource Integrity (SRI) hashes for any scripts loaded from a CDN
- Audit new packages before adding them—check download counts, maintenance activity, and whether the package name could be a typosquat
Quick Reference: Safe vs. Unsafe Patterns
| Unsafe | Safe Alternative |
|---|---|
innerHTML = userInput | textContent = userInput |
eval(str) | JSON.parse(str) |
setTimeout(str, n) | setTimeout(fn, n) |
Token in localStorage | HttpOnly cookie |
message without origin check | Validate event.origin first |
Conclusion
Most JavaScript vulnerabilities follow the same pattern: untrusted data reaches a powerful API without validation. Build the habit of asking “where does this value come from, and what can this API do with it?” That question, applied consistently, catches the majority of issues before they ship.
FAQs
Not always, but it is unsafe whenever you pass it data that originates from user input or any external source. If you must render dynamic HTML, sanitize it first with a library like DOMPurify. For plain text content, use textContent instead, which never interprets markup or executes scripts.
localStorage is accessible to any JavaScript running on the page. If an attacker exploits an XSS vulnerability, they can read everything in storage, including your tokens. HttpOnly cookies are a safer choice because JavaScript cannot access them at all, which limits the damage from client-side attacks.
Reflected XSS involves a malicious payload sent to the server and echoed back in the response. DOM-based XSS never reaches the server. Instead, client-side JavaScript reads from an attacker-controlled source like the URL fragment and writes it into the page unsafely. Both are dangerous, but DOM-based XSS is harder to detect server-side.
CSP tells the browser which script sources are allowed to execute. A strict policy using nonces or hashes blocks inline scripts and unauthorized external scripts from running. Even if an attacker injects malicious markup into the page, the browser refuses to execute it because it does not match the policy.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.