A JavaScript exploit is any technique that abuses how JavaScript is parsed, executed, or trusted — in the browser or in Node.js — to run attacker-controlled logic, and the most damaging ones today rarely involve exotic memory corruption; they abuse the language's own dynamism and the sprawling dependency graph every JS project drags in. If you write or ship JavaScript, the realistic threats are cross-site scripting, prototype pollution, insecure deserialization, and malicious packages — not the browser 0-days that make headlines. This guide walks through each class conceptually, shows what the vulnerable pattern looks like, and focuses on detection and remediation.
Cross-site scripting: the classic JavaScript exploit
XSS is still the most common JavaScript exploit because it turns your own page into the delivery mechanism. The vulnerable pattern is any place where untrusted input reaches the DOM as markup instead of text:
// vulnerable: attacker-controlled `name` becomes live HTML
element.innerHTML = "Welcome, " + user.name;
If user.name contains an image tag with an onerror handler, the browser executes it. The attacker doesn't need to breach your server — they need one reflected or stored input that lands in innerHTML, document.write, or a framework escape hatch like React's dangerouslySetInnerHTML.
The fix is to never build HTML by string concatenation from untrusted data:
- Assign untrusted values with
textContent, notinnerHTML. - Let your framework escape by default and treat every
dangerouslySetInnerHTML/v-htmlas a finding that needs justification. - Sanitize any HTML you genuinely must render with a maintained library like DOMPurify.
- Set a Content-Security-Policy that disallows inline scripts, so an injected
<script>has no way to run even if one slips through.
CSP is the backstop, not the primary control — treat it as defense in depth on top of correct output encoding.
Prototype pollution: a JavaScript exploit unique to the language
Prototype pollution is a JavaScript exploit class that barely exists in other languages. Because almost every object inherits from Object.prototype, an attacker who can set a property named __proto__ (often through a recursive merge or a query-string parser) can inject properties onto every object in the runtime at once.
// naive deep-merge that trusts key names
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === "object") {
merge(target[key] = target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
}
// a payload with a "__proto__" key can now taint Object.prototype
The impact ranges from denial of service to, in the worst case, remote code execution when a polluted property later flows into a template engine or a child-process call. Several widely used utility libraries — including older versions of lodash and various query-string parsers — have shipped prototype pollution advisories over the years. Defend against it by:
- Validating that object keys are not
__proto__,constructor, orprototypebefore merging. - Using
Object.create(null)for maps of untrusted keys so there's no prototype to pollute. - Using
Mapinstead of plain objects for user-controlled key/value data. - Keeping merge and parsing utilities patched, since this is one of the most common CVE categories in the npm ecosystem.
Insecure deserialization and eval-family sinks
Any path that turns a string into executable code is a JavaScript exploit waiting to happen. eval, new Function, setTimeout with a string argument, and vm module misuse all let attacker input become instructions. The same goes for deserializing untrusted data with libraries that reconstruct arbitrary object types.
The remediation is blunt: don't do it. Parse JSON with JSON.parse, never eval. If you need a dynamic expression evaluator, use a sandboxed, purpose-built library rather than the language's own code-execution primitives — the vm module is not a security boundary. Static analysis catches most of these because the dangerous sinks are a short, well-known list, which is one reason a SAST scan flags eval-family calls as high severity by default.
The npm supply chain: exploits you install yourself
The largest JavaScript exploit surface for most teams isn't their own code — it's the hundreds of transitive dependencies a typical node_modules tree pulls in. A single compromised or typosquatted package runs with the full privileges of your build or your app. The event-stream incident in 2018 remains the textbook case: a maintainer handed off a popular package, and the new owner slipped in code that targeted a specific cryptocurrency wallet, riding into thousands of downstream projects through the dependency tree.
Malicious-package techniques worth knowing:
- Typosquatting — a package named
crossenvinstead ofcross-envthat steals environment variables on install. - Install scripts —
preinstall/postinstallhooks that execute the moment you runnpm install, before any of your code runs. - Dependency confusion — publishing a public package with the same name as your private internal one, so the resolver grabs the attacker's version.
Detection here is a dependency problem, not a code problem:
- Commit a lockfile and use
npm ciin CI so builds are reproducible and can't silently pull a new version. - Run
npm auditand, better, a proper software composition analysis tool that maps transitive dependencies and reachability — an SCA tool such as Safeguard can flag that a vulnerable package is present but never actually called, so you patch the exploitable few instead of drowning in noise. - Consider disabling install scripts by default (
npm config set ignore-scripts true) and allow-listing the ones you actually need.
How to build a JavaScript exploit detection pipeline
No single tool covers all four classes, so layer them:
- SAST for XSS sinks,
eval, and prototype-pollution-prone merges in your own code. - SCA for vulnerable and malicious dependencies.
- DAST to confirm reflected/stored XSS against a running app.
- CSP and secure headers as runtime backstops.
Run the first two in the pull request, gate merges on new critical findings with a known fix, and re-scan the dependency tree on a schedule — a package that was clean at merge time can have an advisory published a week later. The goal isn't zero findings; it's making sure a genuinely exploitable JavaScript exploit can't reach production without someone deciding, on the record, to ship it anyway.
FAQ
Is XSS still relevant in modern frameworks like React?
Yes. React escapes output by default, which removes the most common mistakes, but dangerouslySetInnerHTML, href attributes accepting javascript: URLs, and third-party components that build markup all reopen the door. The framework reduces the surface; it doesn't eliminate it.
What's the most overlooked JavaScript exploit class?
Prototype pollution and malicious dependencies. Both are invisible in a code review of your own files — one hides in a utility function's edge case, the other lives entirely in node_modules — which is why automated SCA and SAST matter more than manual review here.
Can Content-Security-Policy stop all JavaScript exploits?
No. CSP is a strong mitigation for injected scripts, but it does nothing for prototype pollution, insecure deserialization, or malicious build-time dependencies. Treat it as one layer, not a replacement for input validation and dependency hygiene.
How do I check my project for known JavaScript vulnerabilities quickly?
Start with npm audit for a first pass, then run a dedicated SCA scan that includes transitive dependencies and reachability analysis so you can prioritize the findings that are actually exploitable from your call graph.