Safeguard
Security

JavaScript Security Issues: Common Risks and How to Fix Them

The JavaScript security issues that bite most teams are XSS, prototype pollution, vulnerable npm dependencies, and leaked secrets. Here is how each works and how to fix it.

Aisha Rahman
Security Analyst
5 min read

The JavaScript security issues that cause the most real-world damage are cross-site scripting, prototype pollution, vulnerable npm dependencies, and secrets leaked into client-side bundles. None of them are exotic. They recur because JavaScript runs in an untrusted environment (the browser), pulls in enormous dependency trees, and makes it easy to mix data and code. This guide covers each class conceptually and shows the fix.

Cross-site scripting (XSS)

XSS is the flagship JavaScript security problem: an attacker gets their script to run in another user's browser, in your site's origin. From there they can read cookies, hijack sessions, and act as the victim.

It happens whenever untrusted data reaches the DOM as markup instead of text. The classic sink is innerHTML:

// Vulnerable: user input parsed as HTML
element.innerHTML = userComment;

If userComment contains an <img src=x onerror=...> tag, the handler runs. The fix is to treat data as data:

// Safe: input rendered as text
element.textContent = userComment;

Framework users get help here — React escapes interpolated values by default, which is why dangerouslySetInnerHTML is named to make you pause. Even library options can be sinks, as the jQuery UI Datepicker showed in CVE-2021-41183, where *Text options rendered HTML.

Defenses: prefer textContent over innerHTML; sanitize any HTML you genuinely must render with a vetted library like DOMPurify; and deploy a Content Security Policy that blocks inline handlers so an injected onerror cannot execute even if markup slips through.

Prototype pollution

Prototype pollution is a JavaScript-specific flaw where an attacker sets properties on Object.prototype, which every object inherits from. A crafted key like __proto__ in a merge or deep-clone operation can inject properties that later code trusts, leading to denial of service, logic bypass, or in some cases remote code execution.

It usually enters through unsafe recursive merges of user-controlled JSON:

// Vulnerable merge lets __proto__ pollute the prototype
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];
    }
  }
}

Fixes: reject or skip the keys __proto__, constructor, and prototype during merges; use Object.create(null) for maps that hold untrusted keys; freeze prototypes where practical; and prefer well-maintained utility libraries whose merge functions already guard against this.

Vulnerable npm dependencies

A modern JavaScript app is mostly npm packages, often hundreds deep. Any one of them can carry a known CVE, and the deep transitive graph means you rarely added the vulnerable package on purpose. This is the largest attack surface for most projects.

Start with the built-in audit:

npm audit --omit=dev

Then fix what you can automatically and pin the rest:

npm audit fix

npm audit reads your lockfile against the advisory database, which is why committing package-lock.json matters — it makes builds reproducible and auditable. For anything audit fix cannot resolve without a breaking change, upgrade the direct dependency or use an override. An SCA tool extends this by resolving the full transitive graph and flagging issues audit misses, and by watching for newly disclosed CVEs in packages you already shipped.

Supply-chain attacks add a second dimension: typosquatted package names, and hijacked maintainer accounts pushing malicious versions. Pin exact versions, review lockfile diffs in code review, and be suspicious of a dependency that suddenly adds install scripts.

Secrets in client-side code

Anything shipped to the browser is public. API keys, tokens, and internal URLs baked into a front-end bundle are readable by anyone who opens dev tools. Teams leak them constantly by putting secrets in environment variables that the bundler inlines.

The rule is simple: no secret belongs in client-side JavaScript. Keep them server-side, proxy privileged calls through your own backend, and scope any key that must be public (like a publishable API key) so it cannot do damage. Scan your bundles and repo history for accidentally committed credentials, since a key in Git history stays there until you rotate it.

Insecure deserialization and eval

Two habits still cause trouble: passing untrusted strings to eval() (or the Function constructor, or setTimeout with a string), and deserializing untrusted data into live objects. Both hand attackers a path to execute code. Never eval untrusted input; parse JSON with JSON.parse, not eval; and validate the shape of any deserialized data before using it.

Putting defenses together

No single control covers JavaScript's risk surface. A layered baseline:

  • Escape by defaulttextContent over innerHTML, and lean on framework escaping.
  • Content Security Policy — a strong CSP is the backstop when an XSS bug slips through.
  • Dependency scanning in CI — gate builds on npm audit plus an SCA tool for transitive coverage.
  • Secret scanning — catch credentials before they land, and rotate anything exposed.

The Academy has deeper walkthroughs of CSP tuning and dependency triage.

FAQ

What is the most common JavaScript security issue?

Cross-site scripting (XSS) is the most prevalent, followed closely by vulnerable npm dependencies. XSS lets an attacker run script in a victim's browser; vulnerable dependencies expose known CVEs through your large transitive package graph.

How do I fix vulnerable npm packages?

Run npm audit to see findings, npm audit fix to apply non-breaking upgrades, and manually upgrade or override the rest. Commit your package-lock.json so builds are reproducible, and use an SCA tool for full transitive coverage.

What is prototype pollution?

A JavaScript-specific flaw where an attacker sets properties on Object.prototype through keys like __proto__ in an unsafe merge, affecting every object. Guard merges against those keys and use Object.create(null) for untrusted-key maps.

Can I safely put an API key in front-end JavaScript?

No. Everything shipped to the browser is readable by users. Keep secrets server-side and proxy privileged calls through your backend; only ever expose keys that are deliberately publishable and tightly scoped.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.