# Phishing

> Phishing is an impersonation attack that tricks a person into entering credentials, approving a login, or running attacker code.

Also known as: Phishing attack, Spear phishing, Spearphishing

Source: https://socket.dev/glossary/phishing

Last updated: 2026-11-03

Phishing is a social engineering attack in which an attacker impersonates a trusted party so a victim enters credentials, approves a login, or runs attacker code. Spear phishing targets one researched person. Phishing works because passwords and one-time codes prove who someone is, not which site they are on. One phished maintainer can ship malware to millions.

Phishing is the most reported cybercrime in the United States. The FBI's Internet Crime Complaint Center logged 191,561 phishing and spoofing complaints in 2025, more than any other category, according to its [2025 annual report](https://www.ic3.gov/AnnualReport/Reports/2025_IC3Report.pdf). Verizon's [2026 Data Breach Investigations Report](https://www.verizon.com/business/resources/executivebriefs/2026-dbir-executive-summary.pdf) puts phishing at 16% of breach entry points. In the software supply chain the target is narrower and the payoff is larger: a package maintainer whose single login can publish code that millions of projects install automatically. Between July and September 2025, lookalike login pages took over maintainer accounts on npm and PyPI and pushed malware into [chalk and debug](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack), [DuckDB](https://socket.dev/blog/duckdb-npm-account-compromised-in-continuing-supply-chain-attack), [eslint-config-prettier](https://socket.dev/blog/npm-phishing-campaign-leads-to-prettier-tooling-packages-compromise), and num2words.

Our position: every account that can publish a package should sign in with an origin-bound credential, meaning a WebAuthn security key or passkey, with no weaker method left enabled as a fallback. That closes the relay attacks of 2025. It does nothing against the 2026 playbook, in which attackers talked the [axios](https://socket.dev/blog/axios-npm-package-compromised) maintainer into installing a remote access trojan during a fake meeting and then rode his live sessions. Once attacker code runs on a maintainer's machine, the defense that remains belongs to everyone downstream: analyze what a new release contains before it installs.

## What is a phishing attack?

A phishing attack is impersonation with a request attached. The attacker borrows the identity of someone the victim already trusts, such as a bank, an employer, a package registry, or a colleague, and asks for something that only makes sense coming from that party: sign in here, confirm this code, pay this invoice, install this update.

The request always serves one of four goals. The attacker wants a credential (a password, a one-time code, a session cookie, an API token), money, code execution on the victim's machine, or consent, such as an OAuth grant to a malicious app. The variants differ in channel and targeting, not in mechanism:

| Variant | Channel | Who it targets | What the attacker wants |
| --- | --- | --- | --- |
| Bulk email phishing | Email | Anyone, at volume | Passwords, card numbers, malware installs |
| Spear phishing | Email, LinkedIn, Slack, GitHub | One researched person | That person's accounts or machine |
| Whaling | Email, phone | Executives | Wire approvals, privileged access |
| Business email compromise (BEC) | Spoofed or hijacked business email | Finance and sales staff | Redirected payments |
| Smishing and vishing | SMS, voice calls | Anyone with a phone | One-time codes, remote-support installs |
| Quishing | QR codes | Mobile users | Credentials typed on unmanaged phones |
| Adversary-in-the-middle (AiTM) | Reverse-proxy copy of a real login page | Accounts protected by passwords plus codes | Live session cookies and one-time codes |
| Consent phishing | OAuth consent or device-code prompt on the real site | Cloud and developer accounts | A token granted to the attacker's app |
| Fake-meeting lure | Chat invite, then a spoofed video call | Developers, founders, maintainers | Malware on the victim's machine |

Business email compromise is the most expensive variant. The FBI recorded $3.05 billion in BEC losses in 2025, second only to investment fraud. Spear phishing is the variant that matters most for open source, because a maintainer's name, email address, and package list are all public. Security teams measure their own exposure with authorized simulations; TrustedSec's open source [Social-Engineer Toolkit (SET)](https://github.com/trustedsec/social-engineer-toolkit) is a long-standing framework for running them.

## How does a phishing attack work?

A phishing attack works in four stages: reconnaissance, pretext, capture, and use. The 2025 attacks on npm maintainers show each stage clearly.

**Reconnaissance.** Attackers do not have to guess who maintains a popular package. The npm registry returns every maintainer's account email in public package metadata:

```bash
$ npm view debug maintainers
[ 'qix <[email redacted]>', 'tootallnate <[email redacted]>' ]
```

We redacted the addresses here; the registry response contains them in full. When Socket reported the [eslint-config-prettier compromise](https://socket.dev/blog/npm-phishing-campaign-leads-to-prettier-tooling-packages-compromise) in July 2025, we noted that threat actors scrape this metadata to build target lists. The [crates.io phishing campaign](https://socket.dev/blog/crates-io-users-targeted-by-phishing-emails) two months later appeared to harvest addresses from Git commit metadata instead, because crates.io does not expose emails in its API.

**Pretext.** The lure imitates the registry's own security mail, because maintainers are trained to act on it. The [npnjs.com phishing email](https://socket.dev/blog/npm-phishing-email-targets-developers-with-typosquatted-domain) asked recipients to log in to verify their account. The lure in the [Qix compromise](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack) warned that outdated 2FA credentials would lock the account and offered an "Update 2FA Now" button. The crates.io lure claimed the registry itself had been breached. Each one came from a lookalike domain: npnjs.com and npmjs.help for npm, pypj.org for PyPI, rustfoundation.dev for crates.io. Swapping one character or the top-level domain is the same trick as [typosquatting](https://socket.dev/glossary/typosquatting) a package name, applied to a domain.

**Capture.** The victim lands on a page that looks exactly like the registry, because it is the registry, relayed through the attacker's server. The next section explains why that relay defeats one-time codes.

**Use.** The attacker converts the captured session into something durable, usually a publish token, and starts releasing versions. That step turns a phished login into a [maintainer account takeover](https://socket.dev/glossary/maintainer-account-takeover), and the versions it produces are indistinguishable from legitimate releases except by their contents.

## Why do one-time codes fail against proxy phishing?

One-time codes fail against proxy phishing because a code is a bearer secret: it proves possession of a seed, and says nothing about where it is being typed. A time-based one-time password (TOTP), as defined in [RFC 6238](https://www.rfc-editor.org/rfc/rfc6238), is an HMAC of a shared secret and the current 30-second window. The whole algorithm fits in a few lines:

```python
import hmac, hashlib, struct, time

def totp(secret: bytes, now: float, step: int = 30, digits: int = 6) -> str:
    counter = int(now // step)                      # same value for 30 seconds
    mac = hmac.new(secret, struct.pack(">Q", counter), hashlib.sha1).digest()
    offset = mac[-1] & 0x0F                         # RFC 4226 dynamic truncation
    code = struct.unpack(">I", mac[offset:offset + 4])[0] & 0x7FFFFFFF
    return str(code % 10**digits).zfill(digits)

# RFC 6238 test vector: secret "12345678901234567890", T = 59 s
print(totp(b"12345678901234567890", 59))            # -> 287082
```

The code depends on exactly two things, the secret and the clock. The domain in the address bar is not one of them. A code typed into npmjs.help is as valid at npmjs.com as a code typed into npmjs.com, for as long as its 30-second window lasts. SMS codes and push approvals share the same property.

An adversary-in-the-middle (AiTM) phishing kit exploits it by reverse-proxying the real site. Every request the victim makes goes to the real registry, every response comes back with the attacker's domain swapped in, and the attacker keeps a copy of the session. DuckDB's maintainers described exactly this after their npm account fell to the same npmjs.help campaign as Qix. Their [advisory](https://github.com/duckdb/duckdb-node/security/advisories/GHSA-w62p-hx95-gf2c) says "the copycat website forwarded all actions to the actual npm website," so the maintainer's 2FA reset really happened on npm. The attackers then added a new API token inside that session and published with it.

PyPI saw the same design in July 2025. Its [incident report](https://blog.pypi.org/posts/2025-07-31-incident-report-phishing-attack/) says the pypj.org server would "transparently proxy requests to PyPI.org." Four accounts were phished, attackers generated two API tokens, and malicious num2words versions 0.5.15 and 0.5.16 were uploaded before PyPI removed them.

> Figure 1. The same proxy against two second factors. A TOTP code is valid wherever it is typed. A WebAuthn signature is bound to the origin the browser is actually on.
>
> A phishing proxy relays a password and TOTP code to npm, but cannot relay a WebAuthn login

Figure 1, in text. On the left, a maintainer signs in with a password and TOTP through a proxy at npmjs.help. The proxy forwards the username and password to npmjs.com, relays npm's request for a 2FA code back to the maintainer, forwards the code (492817 in the diagram) within its 30-second window, and receives a valid session cookie. The proxy then uses that session to create an npm token of its own. On the right, the same maintainer uses a WebAuthn security key. The proxy forwards npm's challenge, which names npmjs.com as the relying party. The browser sees it is running on npmjs.help, refuses to produce a signature for npmjs.com, and throws a SecurityError. If the proxy rewrites the request to name npmjs.help instead, the key holds no credential for npmjs.help and the browser throws NotAllowedError. No signature ever leaves the browser, so the proxy has nothing to relay.

## How WebAuthn breaks the relay

WebAuthn breaks the relay by putting the website's origin inside the thing that gets signed. When a site asks for a login, the browser, not the page, writes a small JSON document called `clientDataJSON` that records the challenge and the origin the request came from. The authenticator signs a hash of that document together with a hash of the relying party ID (RP ID), the domain the credential was registered to. The [WebAuthn specification](https://www.w3.org/TR/webauthn-3/) states the guarantee directly: operations are "scoped to a particular origin" and cannot be replayed against a different one.

The scoping is enforced twice. The browser refuses to request a signature for an RP ID that is not the current domain or a registrable suffix of it, so npmjs.help cannot ask for an npmjs.com signature. The server then checks the signed origin, so a signature produced on npmjs.help would fail verification even if it could be obtained.

We tested this with Chromium's virtual authenticator. We registered a credential on `registry.localhost`, then tried to use it from the lookalike `reglstry.localhost`, which stands in for a phishing domain:

```text
real site   registry.localhost   rpId=registry.localhost
  OK {"type":"webauthn.get","challenge":"c2VydmVyLW5vbmNlLTEyMw","origin":"http://registry.localhost:8001","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}

lookalike   reglstry.localhost   rpId=registry.localhost
  SecurityError: The relying party ID is not a registrable domain suffix of, nor equal to the current domain. Subsequently, an attempt to fetch the .well-known/webauthn resource of the claimed RP ID failed.

lookalike   reglstry.localhost   rpId=reglstry.localhost
  NotAllowedError: The operation either timed out or was not allowed.
```

The first result is the `clientDataJSON` a real login produces; Chrome sometimes inserts the `other_keys_can_be_added_here` field on purpose so servers parse the JSON instead of matching it as a string. The second result is the proxy's problem: the browser will not sign for the real registry from the wrong domain. The third result is the proxy's workaround failing: there is no credential for the lookalike domain to use. This is why PyPI's report says a WebAuthn key would have stopped its July 2025 attack.

Origin-bound [multi-factor authentication (MFA)](https://socket.dev/glossary/multi-factor-authentication-mfa) has real limits, and each one has been exploited:

- **Fallback methods.** A key protects nothing if the account still accepts a TOTP code or a recovery flow the attacker can phish. Under [GitHub's npm security plan](https://socket.dev/blog/package-maintainers-call-for-improvements-to-npm-security-plan), npm stopped accepting new TOTP enrollments in [October 2025](https://github.blog/changelog/2025-09-29-strengthening-npm-security-important-changes-to-authentication-and-token-management/), but existing TOTP setups kept working after that change, and a maintainer with both a key and TOTP can still be relayed through the TOTP path.
- **What happens after login.** WebAuthn protects the sign-in ceremony. The session cookie and any tokens created afterward are ordinary bearer credentials. Malware that copies them from the browser needs no second factor.
- **Consent and device-code flows.** An OAuth consent screen or device-code prompt is a real page on the real domain. Origin binding cannot tell a user approving a legitimate app from one approving a malicious one.
- **Malware on the machine.** An attacker with code execution can wait for the maintainer to touch the key, or simply use the sessions that already exist.

## When phishing skips the login page

The 2026 campaigns against JavaScript maintainers targeted the machine instead of the password. In the axios compromise, the project's [post-mortem](https://github.com/axios/axios/issues/10636) states the root cause plainly: "The attacker gained access to the lead maintainer's PC through a targeted social engineering campaign and RAT malware." The campaign ran for about two weeks before March 31, 2026. As we reported when the [axios maintainer confirmed social engineering](https://socket.dev/blog/axios-maintainer-confirms-social-engineering-behind-npm-compromise), it used a cloned company identity, a staged Slack workspace, and staged meetings before the malicious install. A [remote access trojan (RAT)](https://socket.dev/glossary/remote-access-trojans-rats) on the maintainer's machine let the attacker hijack his active npm and GitHub sessions, so neither 2FA nor OIDC publishing stood in the way.

Axios was one target among many. The [campaign against high-impact Node.js maintainers](https://socket.dev/blog/attackers-hunting-high-impact-nodejs-maintainers) reached maintainers of Fastify, Lodash, dotenv, Express, and Node.js core, as well as several Socket engineers. The pattern was consistent: a LinkedIn or Slack approach from a cloned startup persona, weeks of rapport, a video call on a spoofed Teams or Streamyard page, and then a technical "error" that could only be fixed by installing an app or pasting a terminal command. Security researcher Tay linked the playbook to the North Korea-nexus group that Mandiant tracks as UNC1069, and summarized the difference for maintainers: "This is also really not your typical phishing."

Other lures aimed at developers ran in the same weeks. The [Linux Foundation Slack impersonation campaign](https://socket.dev/blog/attackers-impersonating-linux-foundation-leaders-in-slack-targeting-oss-developers) in April 2026 used a Google Sites page to harvest an email address and verification code, then asked victims to install a malicious root certificate. The [fake VS Code security alert campaign](https://socket.dev/blog/widespread-github-campaign-uses-fake-vs-code-security-alerts-to-deliver-malware) in March 2026 posted thousands of GitHub Discussions about fabricated vulnerabilities, so GitHub's own notification emails delivered the lure.

A maintainer phished into running malware still has to publish a release, and that release still has to be installed to do damage. Downstream defenses get a window. In the axios case it was a short one:

> Figure 2. The axios compromise in UTC. The phishing took two weeks. The malicious dependency was detectable before the poisoned axios release existed, and the releases were live for about three hours.
>
> Timeline of the axios compromise, March 30 to 31, 2026, in UTC

| Time (UTC) | Event | Source |
| --- | --- | --- |
| About two weeks before March 31, 2026 | Social engineering campaign against the lead maintainer begins, ending in a RAT install | axios post-mortem |
| March 30, 05:57:32 | plain-crypto-js@4.2.0 published, a clean copy of crypto-js with no payload | npm registry time field |
| March 30, 23:59:12 | plain-crypto-js@4.2.1 published with a postinstall dropper | npm registry time field |
| March 31, 00:05:41 | Socket flags plain-crypto-js@4.2.1 as malware | Socket axios analysis |
| March 31, 00:21:58 | axios@1.14.1 published from the maintainer's account, adding plain-crypto-js as a dependency | npm registry time field |
| March 31, 01:00:57 | axios@0.30.4 published on the legacy 0.x line | npm registry time field |
| March 31, 03:15 | Both malicious axios versions removed from npm | axios post-mortem |
| March 31, 03:25:11 | npm replaces plain-crypto-js with a security holding package | npm registry time field |

The registry still records the dependency's history, even though npm replaced the package with a security placeholder:

```bash
$ curl -s https://registry.npmjs.org/plain-crypto-js | jq '.time, .description'
{
  "created": "2026-03-31T04:26:32.907Z",
  "modified": "2026-03-31T04:26:35.731Z",
  "4.2.0": "2026-03-30T05:57:32.867Z",
  "4.2.1": "2026-03-30T23:59:12.278Z",
  "0.0.1-security": "2026-03-31T03:25:11.636Z",
  "0.0.1-security.0": "2026-03-31T04:26:33.055Z"
}
"security holding package"
```

The detail that matters is ordering. Socket's malware detection flagged plain-crypto-js@4.2.1 at 00:05:41 UTC, six and a half minutes after it was published and 16 minutes before the poisoned axios@1.14.1 that depended on it existed. A defense that reads package contents had a verdict before anyone could have installed axios@1.14.1. A defense that waits for a CVE never would: malware gets no CVE.

## Which defenses stop which phishing lures?

Origin-bound authentication stops every phishing lure that ends at a login page and none that end in code execution. Analyzing the published package stops the payload regardless of how the maintainer was fooled. The incidents above sort cleanly:

| Incident | Lure | What the attacker captured | WebAuthn on the maintainer account | Inspecting the release before install |
| --- | --- | --- | --- | --- |
| eslint-config-prettier, July 2025 | Email from npnjs.com linking to a cloned npm login | An npm token | Stops it | Applies: the versions carried a Windows DLL loader |
| num2words on PyPI, July 2025 | Verification email linking to a pypj.org proxy | Credentials for four accounts, then two API tokens | Stops it, per PyPI's incident report | Applies: the uploads contained malware |
| chalk, debug, and 17 more (Qix), September 2025 | "Update 2FA Now" email from npmjs.help | Password and a live TOTP code | Stops it | Applies: every version carried the same wallet drainer |
| DuckDB, September 2025 | The same npmjs.help proxy | A session, then a new API token | Stops it | Applies: the same wallet drainer |
| crates.io users, September 2025 | "Infrastructure compromised" email from rustfoundation.dev | GitHub credentials | Stops it | Not reached: no malicious crates were reported |
| axios, March 2026 | Cloned company, staged Slack workspace, fake meeting | A RAT, then live npm and GitHub sessions | No effect | Applies: the dependency was flagged before axios@1.14.1 was published |
| Node.js maintainers, March to April 2026 | The same fake-meeting playbook | Attempted malware installs | No effect | Not reached: none of these attempts is known to have produced a release |
| Beamglea, October 2025 | HTML attachment loading an npm-hosted redirect | Office 365 passwords of non-developers | Not applicable: the attacker used its own npm accounts | Applies: the packages held redirect scripts with hard-coded victim emails |

Our verdict: maintainers should treat a security key as the floor, not the finish. It closes the attack that worked against npm and PyPI in 2025, and the attackers have already moved on. Consumers should assume any maintainer can be phished and judge each new version by its contents. A [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) helps because the axios versions were live for about three hours. Blocking install scripts helps too: [npm v12](https://socket.dev/blog/npm-12), released in July 2026, skips dependency lifecycle scripts unless you approve them, which would have stopped the plain-crypto-js `postinstall` dropper. It would not have stopped the Qix wallet drainer, which ran in end users' browsers when the poisoned code was bundled into websites. [Package provenance](https://socket.dev/glossary/package-provenance) and [npm trusted publishing](https://socket.dev/glossary/npm-trusted-publishing) help by making an out-of-pipeline release visible, as axios@1.14.1 was. Neither one helps when the attacker publishes through the maintainer's real pipeline with the maintainer's real session.

## Package registries as phishing infrastructure

Phishing also uses the supply chain in the other direction: registries serve as free, trusted hosting for ordinary credential phishing aimed at people who are not developers at all. Any public npm package is automatically served over HTTPS by CDNs such as unpkg.com and jsDelivr, from domains that corporate filters rarely block.

The [Beamglea campaign](https://socket.dev/blog/175-malicious-npm-packages-host-phishing-infrastructure), which our Threat Research Team documented in October 2025, published 175 npm packages with names like `redirect-xs13nr`. None ran code on install. Each held a redirect script that a purchase-order-themed HTML attachment loaded from unpkg.com, sending the victim to an Office 365 credential page with their email address pre-filled. The lures were addressed to more than 135 people across over 100 industrial, technology, and energy organizations.

A [separate spearphishing campaign](https://socket.dev/blog/spearphishing-campaign-abuses-npm-registry) in December 2025 shipped entire lure pages inside 27 packages, aimed at 25 named individuals, mostly sales staff in manufacturing and healthcare. The pages blocked headless browsers, required real mouse or touch input before proceeding, and handed off to redirectors that match documented infrastructure for Evilginx, an open source AiTM toolkit that captures session cookies rather than just passwords.

For anyone inspecting packages, the signal is a mismatch between the package and its contents: an HTML login page, a `document.write()` that replaces the whole DOM, or a hard-coded email address and redirect URL in a package with no plausible library purpose. That is what a [malicious package](https://socket.dev/glossary/malicious-package) looks like when its victim never runs `npm install`.

## What should change

Registries have made real progress. After the 2025 phishing wave, npm revoked classic tokens, blocked new TOTP enrollments, and capped write-token lifetimes at 90 days. PyPI's incident report told every maintainer to use WebAuthn. Two gaps remain from the attacks above.

First, sensitive actions should not trust an existing session. DuckDB's attacker turned a relayed session into an API token that outlived the phishing page. Creating a token, changing 2FA, or adding a maintainer should require a fresh WebAuthn assertion every time. A fresh TOTP prompt would not help, because the proxy relays it too. Publishers of widely used packages should be required to use origin-bound credentials, with TOTP removed rather than merely discouraged.

Second, no registry control distinguishes a maintainer from an attacker sitting on that maintainer's laptop. That is the attack that worked in 2026, and the people best placed to catch it are the ones about to install the result. The login page is no longer the perimeter. The maintainer's machine is, and the last check anyone controls is reading the code before it runs.
