Clickjacking and Supply Chain Attacks
TL;DR
Two attack classes that aren’t injection-based. Clickjacking = attacker iframes your site, overlays misleading UI, tricks user into clicking a real button on your site (transferring money, deleting account, granting permission). Defense: X-Frame-Options: DENY or frame-ancestors. Supply chain attacks = the npm package, CDN script, or build tool your app depends on is compromised; malicious code ships to your users via your trusted bundle. Defense: dependency auditing, Subresource Integrity, lockfile pinning, build provenance.
Clickjacking Q&A
Q: How does clickjacking work?
A: Attacker creates a page that iframes your site at very low opacity (or absolute-positioned offscreen) and overlays misleading UI:
<!-- evil.com -->
<style>
iframe { position: absolute; opacity: 0.001; width: 600px; height: 400px; }
button { position: absolute; top: 200px; left: 200px; }
</style>
<button>Click here for a free iPhone!</button>
<iframe src="https://yourbank.com/transfer?to=attacker&amount=1000"></iframe>
User sees the “Free iPhone” button, clicks. The click actually lands on the (invisible) iframe’s “Confirm Transfer” button. The transfer happens because the user is logged into yourbank.com (cookies attach to the iframe’s requests).
Variations:
- Likejacking — Facebook “Like” hijacked.
- Cursor-jacking — fake cursor.
- Drag-and-drop jacking — tricked into dragging data.
Q: Defenses.
A: Two HTTP headers prevent your site from being framed by other origins:
Content-Security-Policy: frame-ancestors 'none' # modern, granular
X-Frame-Options: DENY # legacy, ignored if CSP frame-ancestors is set
frame-ancestors is more flexible:
frame-ancestors 'none'— never frame me.frame-ancestors 'self'— only same-origin frame.frame-ancestors 'self' https://trusted.partner.com— me + one partner.
Set both for compatibility (X-Frame-Options for legacy browsers; frame-ancestors for modern).
Q: When should a site be frameable?
A:
- Embeddable widgets (Stripe checkout iframe, YouTube embeds, Twitter cards). These intentionally allow
frame-ancestors *. - Documentation embedded in dashboards.
- OAuth consent screens that pop up in popups (not iframes — but watch for
window.openpatterns).
For everything else — default deny. Your app’s main UI should never be frameable.
Q: JavaScript framebusting — old technique.
A: Before headers were standardized, sites used:
if (top !== self) {
top.location = self.location; // break out of iframe
}
Attacker bypasses: sandbox="allow-scripts" on the iframe (no allow-top-navigation) blocks the framebust. Sometimes attackers use allow-forms to submit, etc.
Don’t rely on JS framebusting alone. Use the headers. JS is a fallback at best.
Q: UI redress attacks beyond clickjacking.
A:
-
Tab nabbing —
<a target="_blank" href="...">opens in a new tab; the new tab can usewindow.opener.location = "https://evil.com/login"to redirect the original tab to a phishing page. Defense:rel="noopener noreferrer".<a href="https://example.com" target="_blank" rel="noopener noreferrer">Link</a>Modern browsers (Chrome 88+) treat
target="_blank"as implicitnoopener, but set explicitly for older browsers. -
Cursor jacking — fake cursor overlay misleads click target.
-
Confused deputy — your trusted app makes a request on behalf of an untrusted source (e.g., your API takes a user-supplied URL and fetches it server-side → SSRF). Less “frontend” but the pattern is the same shape.
Supply Chain Q&A
Q: What’s a supply chain attack?
A: A compromise of a dependency (npm package, CDN-hosted library, build tool, browser extension) that flows malicious code into your app — which then runs in your users’ browsers under your origin.
Famous examples:
- event-stream (2018) — popular npm package was transferred to a malicious maintainer who added crypto-wallet-stealing code targeting one app.
- ua-parser-js (2021) — maintainer’s npm credentials stolen; malicious version published.
- PolyKill (2024) — CDN-hosted JS library compromised when a malicious actor bought the polyfill.io domain.
- node-ipc (2022) — maintainer deliberately added geofenced malicious code.
The attacker doesn’t compromise you — they compromise something you trust.
Q: Defenses against npm supply chain.
A:
-
Lockfile (
package-lock.json,pnpm-lock.yaml,yarn.lock) — pin exact versions; commit.npm ci/pnpm install --frozen-lockfilein CI fails on lockfile drift. -
npm audit/pnpm auditin CI — checks against the Advisory Database. Fail builds on known vulnerabilities. -
Snyk / Dependabot / Renovate — automated CVE scanning + PR-based updates. Renovate is open-source; Dependabot is GitHub-native.
-
overrides/resolutionsinpackage.json— pin a transitive dep to a patched version when the upstream is slow to update:"overrides": { "vulnerable-pkg": "1.2.4" } -
Provenance — npm supports provenance attestation (since 2023). Packages published with
--provenanceare linked to their build environment (GitHub Actions etc.). Verify withnpm view <pkg> --json | jq .repository. -
Minimize dependencies — every dep is a trust relationship. Audit before adding.
-
Lockfile review on PRs — when a PR changes the lockfile, review the diff. Look for new packages you don’t recognize.
-
Avoid
*and^for security-critical deps — pin exact versions.
Q: Defenses against CDN compromise.
A:
-
Subresource Integrity (SRI) —
<script src="..." integrity="sha384-...">. Browser verifies the hash; refuses to execute if changed:<script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K..." crossorigin="anonymous"></script> -
Self-host — instead of CDN, copy the script into your bundle. No SRI needed because you served it. Trade: lose CDN cache benefits.
-
Pin a version + SRI hash — never auto-upgrade.
-
CSP
script-srcto allow-list specific CDN origins. -
COEP: require-corp+CORPheaders — opt-out subresources require explicitCross-Origin-Resource-Policyheaders. Defense in depth.
Q: Build tool / compiler attacks.
A: Webpack plugins, Vite plugins, esbuild plugins, Babel plugins, PostCSS plugins — all run at build time with full Node access. A malicious plugin can:
- Read your
.env(API keys, secrets). - Inject code into the build output.
- Exfiltrate via build-time fetch.
Defenses:
- Audit plugins before adding.
- Use CI build environments (GitHub Actions) with restricted env access.
- Lockfile pinning of dev deps too.
- Reproducible builds — verify the deployed artifact matches the source.
Q: The 2024 polyfill.io incident — what happened?
A: polyfill.io was a popular CDN service that served JS polyfills based on user-agent detection. In early 2024, the domain was sold to a Chinese-owned company that started serving malicious JavaScript via the CDN.
Sites that had <script src="https://polyfill.io/v3/polyfill.min.js"></script> (without SRI) shipped attacker code to their users.
Lessons:
- SRI on every external script.
- Self-host critical deps when possible.
- Monitor for domain ownership changes (rare but happens).
- Modern browsers don’t need polyfills for most things — audit your
browserslist.
Q: Browser extension threat.
A: Extensions installed on the user’s browser can read DOM, modify pages, intercept network requests. A malicious extension defeats nearly all your security measures.
You can’t fix this from your app. Mitigations:
- CSP
script-srcwon’t block extensions (intentional — they’re “trusted by user”). - Detect anomalous DOM mutations and warn (limited).
- Sensitive ops require re-auth so even if the extension takes the session, fresh consent gates the action.
Q: Dependency confusion attack.
A: A specific npm attack: a company has a private package @mycompany/util. Attacker publishes mycompany/util (no scope, or same scope on public npm with higher version) on public npm. If your package manager isn’t configured strictly, it pulls from public registry → attacker’s code runs in your build.
Defenses:
- Use scoped packages for internal modules (
@mycompany/...). - Configure npm/pnpm registry correctly — internal scope only resolves from internal registry.
packageManagerfield in package.json +.npmrcenforcing scopes.
Q: How do you do a real supply chain audit?
A:
- List direct dependencies (
package.json). - List transitive (
pnpm list --depth=Infinity). - For each, ask:
- Is the maintainer trustworthy / active?
- Recent releases? Last update?
- Open security issues?
- Many GitHub stars but only one maintainer = bus factor risk.
- Replace risky deps with well-maintained alternatives or vendor-copy.
- Set up automated dep auditing (Snyk, Dependabot, Renovate) so this isn’t one-shot.
For a large org: SBOM (Software Bill of Materials) generated per release. Standard formats: SPDX, CycloneDX. Lets you respond fast to a “is X library affected” question.
Gotchas / edge cases
X-Frame-Options: SAMEORIGIN— frameable from same origin only. Useful for in-app embeds.X-Frame-Options: ALLOW-FROM uri— deprecated; ignored by Chrome. Useframe-ancestors.window.openerontarget="_blank"— setrel="noopener noreferrer". Modern browsers default to noopener but be explicit.postMessagewithout origin check is a vector — attacker frames your site and sends malicious messages to it.npm auditfalse positives — sometimes reports vulns in transitive deps that don’t affect you. Investigate before fixing.- CDN-hosted “polyfills” without SRI — historical footgun, see polyfill.io. SRI on everything external.
- Build provenance is only useful if consumers verify; default npm install doesn’t enforce.
What a senior is expected to say
- “Clickjacking: attacker iframes your site, overlays misleading UI, tricks user clicks. Defense:
frame-ancestors 'none'(CSP) +X-Frame-Options: DENYfor compat.” - “
target=\"_blank\"needsrel=\"noopener noreferrer\"— older browsers let the new tab redirect the opener (phishing redirect).” - “Supply chain attacks compromise deps not you. Defenses: lockfile pinning,
npm auditin CI, automated dep updates (Renovate/Dependabot), Subresource Integrity on external scripts.” - “polyfill.io (2024) is the canonical example — domain sold, malicious code served to thousands of sites that didn’t have SRI.”
- “Build-time plugins have full Node access — audit before adding, restrict CI env access.”
- “Dependency confusion: scope internal packages (@yourorg/…), configure registry strictly so the scope resolves only internally.”
Cross-references
- Security headers (where frame-ancestors lives): 05_sri_hsts_security_headers.md
- CSP for script source allowlists: 03_csp.md
- Package managers + lockfile semantics: ../09_build_tools/07_package_managers.md
Further reading
- OWASP — Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
- npm — Supply chain security: https://docs.npmjs.com/about-audit-reports
- web.dev — Cross-Origin-Opener-Policy: https://web.dev/articles/why-coop-coep
- polyfill.io incident write-up (search “polyfill.io 2024” for various sources)