The JavaScript security vulnerabilities that cause real damage cluster around a handful of patterns — cross-site scripting, prototype pollution, regular-expression denial of service, insecure dependencies, and unsafe use of eval-style constructs — and each has a well-understood defense. JavaScript's reach makes this worth getting right: it runs in every browser, on servers through Node.js, and increasingly in edge and serverless environments, so a single bad pattern can be exploitable from multiple directions. This is a practitioner's tour of the classes that matter and how to detect them before they ship.
Cross-site scripting is still number one
XSS remains the most common serious JavaScript vulnerability. It happens when untrusted data reaches the DOM in a way that lets an attacker execute script in another user's browser, stealing sessions, keystrokes, or anything the page can touch.
The dangerous sinks are the ones that interpret strings as markup or code. Assigning user input to innerHTML is the classic:
// Vulnerable: user input parsed as HTML
element.innerHTML = userInput;
Prefer textContent, which never parses markup:
// Safe: treated as plain text
element.textContent = userInput;
Frameworks help enormously here. React escapes interpolated values by default, so {userInput} in JSX is safe — until someone reaches for dangerouslySetInnerHTML, which turns escaping off. Angular and Vue have equivalent escape hatches (bypassSecurityTrustHtml, v-html). Every use of those is a review point. Where you genuinely must render user HTML, sanitize it with a maintained library like DOMPurify rather than a hand-rolled regex, and add a Content-Security-Policy header so a missed sink is harder to weaponize.
Prototype pollution: a JavaScript-specific trap
Prototype pollution is a bug class that barely exists outside JavaScript. Because objects inherit from Object.prototype, code that recursively merges untrusted data into an object can be tricked into writing to __proto__, changing the prototype that every object shares. That can flip security-relevant defaults, inject properties into objects the attacker never touched, or escalate into remote code execution in the right conditions.
The vulnerable pattern is an unsafe deep merge or property-setter that trusts attacker-controlled keys:
// Vulnerable: attacker sends { "__proto__": { "isAdmin": true } }
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
Defenses: reject or strip __proto__, constructor, and prototype keys before merging; use Object.create(null) for objects that hold untrusted data so they have no prototype to pollute; use Map instead of plain objects for key-value stores keyed by user input; and freeze Object.prototype in security-sensitive services. Several high-profile npm packages have shipped prototype pollution CVEs, so scanning dependencies matters as much as auditing your own merge logic.
Regular-expression denial of service (ReDoS)
A regular expression with a certain structure can take exponential time to evaluate against a crafted input. Because JavaScript is single-threaded, one such regex processing attacker input freezes the entire event loop — no other request gets served until it finishes, which for a bad case is effectively never. That is ReDoS.
The trigger is "catastrophic backtracking," usually from nested or overlapping quantifiers like (a+)+$. Real-world ReDoS bugs have hit widely used packages; CVE-2022-25844 in AngularJS is one example, where an attacker-controlled locale value produced a runaway regex. Defenses are to avoid nested quantifiers, cap input length before matching, prefer non-backtracking approaches or a linear-time regex engine (like the RE2 bindings) for untrusted input, and lint your patterns with a ReDoS-aware tool. When a vulnerable regex lives in a dependency, upgrading or replacing the package is the fix.
Insecure dependencies are the biggest surface
The average JavaScript project pulls in hundreds of transitive packages from npm. That dependency tree is, statistically, where most of your exploitable vulnerabilities live — not in your own code. Malicious package publishes, typosquats, abandoned maintainers, and plain old known CVEs all ride in through node_modules.
Start with the built-in audit:
npm audit
But treat that as a floor, not a ceiling. npm audit reports against the advisory database and is noisy about dev-only and unreachable issues. For prioritization by whether a vulnerability is actually reachable in your build, and for continuous coverage across many repos, an SCA tool such as Safeguard can trace the transitive path and tell you which direct dependency to bump. Commit your lockfile, update dependencies on a schedule rather than in a panic, and generate a software bill of materials so a newly disclosed CVE can be matched against what you actually ship in minutes. Our npm security best practices post goes deeper on locking down the supply chain specifically.
Avoid dynamic code execution
eval, the Function constructor, and setTimeout/setInterval called with a string argument all execute arbitrary strings as code. If any part of that string derives from user input, you have handed an attacker a code-execution primitive. There is almost never a legitimate reason to run these on untrusted data. Replace eval-based JSON parsing with JSON.parse, replace dynamic property access built from strings with explicit lookups or a Map, and lint for these constructs so they cannot creep back in. A Content-Security-Policy that omits unsafe-eval also blocks a whole category of injected-script execution in the browser.
Tie it together with automated scanning
No developer catches every one of these by reading code. The durable approach is to make detection part of the pipeline: a SAST scanner for the code-level patterns (XSS sinks, prototype pollution, dangerous eval), an SCA scanner for dependency CVEs, and a linter tuned for ReDoS and unsafe constructs. Run all three in CI so a regression is caught on the pull request rather than in production. The application security scanning guide covers how those tools divide the work.
FAQ
What is the most common JavaScript security vulnerability?
Cross-site scripting (XSS) is the most common high-impact issue in browser-facing JavaScript. It occurs when untrusted data reaches a DOM sink like innerHTML without escaping, allowing an attacker to run script in a victim's browser.
What is prototype pollution?
Prototype pollution is a JavaScript-specific vulnerability where attacker-controlled keys (especially __proto__) are written into Object.prototype during an unsafe merge, changing the prototype every object inherits from. It can flip security defaults or escalate to code execution.
Are most JavaScript vulnerabilities in my own code or my dependencies?
Statistically, most exploitable vulnerabilities live in transitive npm dependencies rather than first-party code, because a typical project pulls in hundreds of packages. Dependency scanning is therefore as important as auditing your own logic.
How do I detect JavaScript security vulnerabilities before shipping?
Run SAST for code-level patterns, SCA for dependency CVEs, and a ReDoS-aware linter, all wired into CI so issues are caught on the pull request. Commit your lockfile and generate an SBOM so newly disclosed CVEs can be matched against what you ship.