Back to glossary

Hashing

Hashing maps data of any size to a fixed-length hash. A cryptographic hash like SHA-256 detects any change to bytes, but not who made them or if they are safe.

Also known as hash function, cryptographic hash, message digest, digest, checksum, SHA, Secure Hash Algorithm, SHA-256, SHA-1, MD5, message digest algorithm, integrity hash

Hashing is running data through a hash function that returns a short, fixed-length value called a hash, or message digest. Cryptographic hash functions such as SHA-256 make it infeasible to find two inputs with the same hash, so a hash identifies exact bytes. Package managers, Git, and container registries use hashes to detect tampering.

A hash check answers exactly one question: are these the bytes I expected? It cannot tell you whether those bytes are safe. It is also only as trustworthy as the place the expected hash came from. In the axios compromise in March 2026, the malicious axios 1.14.1 tarball carried a correct SHA-512 integrity hash, and every lockfile that resolved it recorded that hash faithfully. Our position: a hash moves trust, it does not create it. Pin hashes everywhere, then analyze what the hashed bytes actually do.

What is hashing in cybersecurity?

#

Hashing in cybersecurity is the standard way to identify exact bytes and detect any change to them. A hash function reads input of any length and returns a fixed-size output: 256 bits for SHA-256, 512 bits for SHA-512. The same input always produces the same hash. Changing one character changes roughly half the output bits. In this pair, 131 of the 256 bits differ:

Bash
$ printf 'hello' | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824  -
$ printf 'hellp' | sha256sum
fdd7585e08c4e2afd71dcabdb4636c89d557a3f42db9e2040c8bbd1708aa4ce7  -

A cryptographic hash function adds three security properties. Each one names an attack the function must resist:

  • Preimage resistance. Given a hash, nobody can find an input that produces it. This is why hashes are called one-way.
  • Second-preimage resistance. Given a specific file, nobody can find a different file with the same hash. A pinned download relies on this property.
  • Collision resistance. Nobody can find any two inputs with the same hash, even two inputs crafted together. For an n-bit hash, a generic birthday search takes about 2^(n/2) attempts, so SHA-256 offers about 2^128.

Non-cryptographic hash functions, such as CRC32 in ZIP archives or the functions inside hash tables, are deterministic and fixed-size but were never designed to resist an attacker. They catch accidental corruption, not deliberate edits.

The difference between collisions and second preimages matters more than most explanations admit. A second-preimage attack lets an outsider replace a file someone else hashed. A collision attack helps only someone who controls both inputs from the start: they get the harmless version reviewed, signed, or pinned, then ship the other version under the same hash. In open source, the party who prepares both inputs is the package author. That is exactly the threat model a software supply chain has.

How is hashing different from encryption?

#

Hashing differs from encryption because a hash function has no key and no inverse. Encryption is built to be reversed by whoever holds the key. A hash is built never to be reversed by anyone. You encrypt data you need back. You hash data you only need to recognize.

A bare hash also says nothing about who produced it. Anyone can hash anything, including an attacker who replaces both a file and its published hash. Two constructions add identity on top of a hash. An HMAC (hash-based message authentication code) mixes a secret key into the hash, so only key holders can produce a valid tag. A digital signature hashes the data and signs the hash with a private key, so anyone with the public key can check it. npm's registry signature, for example, covers the string name@version:integrity, where integrity is the tarball's SHA-512 hash. Our code signing entry walks through verifying one.

Password storage is the one place where a fast, strong hash is the wrong tool. A stolen table of SHA-256 password hashes lets an attacker test guesses as fast as SHA-256 runs. Password hashing functions are salted and deliberately slow. The OWASP Password Storage Cheat Sheet recommends Argon2id with at least 19 MiB of memory and 2 iterations, or PBKDF2-HMAC-SHA256 with 600,000 iterations.

PrimitiveKeyReversibleProvesDoes not proveUse it for
Hash (SHA-256, SHA-512)NoneNoBytes match an expected hashWho made the bytes, or whether they are safePinning artifacts, content addressing
Password hash (Argon2id)None; a per-password saltNoA guess matches the stored passwordAnything about data integrityStored credentials only
HMAC-SHA256Shared secretNoA key holder produced this exact messageWhich key holder; nothing to third partiesWebhooks, API request signing, session cookies
Digital signature (ECDSA, Ed25519)Private and public key pairNoThe private-key holder approved these bytesThat the key was not stolen, or that the code is safeReleases, registry metadata, Git tags
Encryption (AES-GCM)Secret keyYesConfidentiality, plus integrity for authenticated modesWho encrypted, unless keys map to identitiesData at rest and in transit

Verdict: use a plain hash when the expected value reaches you through a channel the attacker cannot write to, such as a reviewed file in your own repository. Use a signature when it cannot. Use a password hash only for passwords. Never use encryption when you only need to recognize data.

MD5, SHA-1, SHA-2, and SHA-3: which hash algorithms are still safe?

#

The SHA-2 and SHA-3 families are safe for security use as of November 2026; MD5 and SHA-1 are broken. MD5, Ron Rivest's Message Digest 5, was published in 1992. SHA, the Secure Hash Algorithm, is the NIST-standardized family that replaced it: SHA-1 in 1995, SHA-2 (including SHA-256 and SHA-512) in 2001, and SHA-3 in 2015.

AlgorithmOutputStatusBest known attackWhere you still meet it
MD5128 bitsBroken for collisionsCollisions in seconds on a PC; a chosen-prefix collision forged a code-signing certificate in 2012Legacy checksums and mirrors
SHA-1160 bitsBroken for collisions; NIST phase-out by December 31, 2030SHAttered (2017), about nine quintillion computations; Shambles (2020), chosen-prefix at 2^63Git object IDs, GitHub Actions SHA pins, npm's legacy shasum field
SHA-256, SHA-512 (SHA-2)256 or 512 bitsSafeNo practical attacknpm integrity, PyPI file hashes, Go go.sum, container image digests
SHA-3224 to 512 bitsSafeNo practical attackRare in package ecosystems; a structurally different backup to SHA-2

MD5 collisions take "10 seconds or less" on a 2.6 GHz Pentium 4, according to RFC 6151, which declared MD5 unacceptable wherever collision resistance is required. Collision attacks have already defeated code signing. In 2012 the Flame malware carried a forged certificate that chained to Microsoft's root. Microsoft's analysis found the attackers "had to perform a collision attack" to forge it, against a certificate signed with md5RSA.

SHA-1 fell on February 23, 2017, when researchers at CWI Amsterdam and Google published SHAttered: two different PDF files with the same SHA-1 hash, produced with roughly nine quintillion SHA-1 computations. NIST has since set December 31, 2030 as the date to stop using SHA-1. "We recommend that anyone relying on SHA-1 for security migrate to SHA-2 or SHA-3 as soon as possible," said NIST computer scientist Chris Celi in the retirement announcement.

Neither break is a second-preimage attack. The best preimage attack on MD5 still costs 2^123.4 operations, per RFC 6151. An MD5 hash you computed yourself still detects a third party swapping your file. It fails exactly where the supply chain needs it most: when the file's author is the adversary. Use SHA-256 or SHA-512 for anything new, and treat MD5 and SHA-1 values as identifiers, never as security controls.

How package managers use hashes

#

Every major package ecosystem pins dependencies by hash. They hash different things and get the expected value from different places, and that second difference decides what the check protects.

npm

#

The npm registry stores a SHA-512 hash of every tarball as dist.integrity, written in Subresource Integrity format: the algorithm, a dash, and the base64-encoded hash. The older shasum field is a hex SHA-1 of the same tarball. Anyone can recompute both:

Bash
$ curl -s https://registry.npmjs.org/ms/2.1.3 | jq '.dist | {shasum, tarball, integrity}'
{
  "shasum": "574c8138ce1d2b5861f0b44579dbadd60c6615b2",
  "tarball": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
$ curl -sO https://registry.npmjs.org/ms/-/ms-2.1.3.tgz
$ echo "sha512-$(openssl dgst -sha512 -binary ms-2.1.3.tgz | base64 -w0)"
sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
$ sha1sum ms-2.1.3.tgz
574c8138ce1d2b5861f0b44579dbadd60c6615b2  ms-2.1.3.tgz

package-lock.json copies that value into each entry, and npm ci refuses a tarball that does not match. We changed one character of the recorded hash and ran npm 12.2.2 with an empty cache:

Plain text
npm warn tarball tarball data for ms@https://registry.npmjs.org/ms/-/ms-2.1.3.tgz (sha512-7Flz…) seems to be corrupted. Trying again.
npm error code EINTEGRITY
npm error sha512-7Flz… integrity checksum failed when using sha512: wanted sha512-7Flz… but got sha512-6Flz…. (2967 bytes)

PyPI

#

PyPI publishes a SHA-256 hash for every file in its JSON Simple API:

Bash
$ curl -s -H 'Accept: application/vnd.pypi.simple.v1+json' https://pypi.org/simple/six/ \
    | jq '.files[] | select(.filename=="six-1.17.0-py2.py3-none-any.whl") | .hashes'
{
  "sha256": "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"
}

pip's hash-checking mode turns on when any line in a requirements file carries --hash=sha256:…. From then on every package, transitive ones included, must be pinned with == and hashed, or pip refuses to install. pip deliberately ignores hashes it gets from the index for this purpose. "Since this hash originates remotely, it is not a useful guard against tampering," the pip documentation says.

Go

#

Go modules record two hashes per dependency in go.sum: one for the module's file tree and one for its go.mod. The h1: prefix names the algorithm, which Go's dirhash package defines as a SHA-256 over a sorted list of per-file SHA-256 hashes. We fetched go-cmp v0.6.0 directly from GitHub, bypassing Go's module proxy, and reproduced the hash that golang.org/x/tools records in its go.sum:

Bash
$ grep 'go-cmp v0.6.0 ' go.sum
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
# inside the unpacked module zip
$ sha256sum $(find github.com -type f | LC_ALL=C sort) | openssl dgst -sha256 -binary | base64
ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

When go.sum has no entry for a module, the go command checks the hash against sum.golang.org, which describes itself as "an append-only transparency log."

Git and GitHub Actions

#

Git names every object by its hash. A file's ID is the SHA-1 of a short header plus its contents, and a commit's ID covers its tree and its parents, so one commit hash pins an entire history:

Bash
$ printf 'hello\n' | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
$ printf 'blob 6\0hello\n' | sha1sum
ce013625030ba8dba906f756967f9e9ca394464a  -

That is why pinning a GitHub Action to a full commit SHA protects you and pinning to a tag does not. A tag is a movable pointer. In the Trivy GitHub Actions compromise in March 2026, we found the attacker had force-pushed 75 of the 76 version tags of aquasecurity/trivy-action. In the tj-actions/changed-files compromise a year earlier, roughly 23,000 repositories referenced the action, and every one that pinned by SHA was unaffected. As our GitHub Actions hardening guide puts it, a SHA proves immutability, not provenance: a commit pushed to a fork can be fetched through the upstream repository's URL. Our GitHub Actions security entry covers those impostor commits.

Where does the expected hash come from?

#
MechanismWhat is hashedAlgorithmWhere the expected hash comes fromWho checks
npm lockfileRegistry tarballSHA-512The registry, at first resolution; afterward, your committed lockfilenpm ci, on every install
pip hash-checking modeEach wheel or sdistSHA-256 recommendedA requirements file you generate and commitpip, all or nothing
Go modulesModule file tree and go.modSHA-256 (h1:)go.sum; for a new module, the public checksum databaseThe go command, on every download
GitHub Actions SHA pinCommit, covering tree and historySHA-1The workflow file in your repositoryThe runner, at checkout
Container image digestImage manifestSHA-256Your deployment config (image@sha256:…)The container runtime, on pull
Downloaded scriptThe scriptWhatever the vendor publishesThe vendor's published checksumOnly you, if you wrote the check

Verdict: the strongest setups keep the expected hash in a reviewed file in their own repository and treat the moment a hash is first recorded as a security event. Of the mechanisms above, only Go checks a first-seen hash for a public module against an independent public log by default.

What does a matching hash prove?

#

A matching hash proves that the bytes are identical to bytes someone hashed earlier, and nothing more. Every hash check implicitly trusts whoever supplied the expected value. Attackers defeat hash checks by controlling that supply, not by breaking SHA-256. Figure 1 shows the three cases.

  1. 1Bytes change

    Check fails. Attack caught.

    Corrupted download, tampered mirror, a lockfile hash that no longer matches (npm: EINTEGRITY).

  2. 2Bytes and hash change together

    Check passes. Attack missed.

    Lockfile edit rewriting resolved URL and integrity, checksum hosted beside the download, Git tag repointed at a new commit.

  3. 3Malicious before hashing

    Check passes on every install.

    Compromised maintainer publishes a new version; release workflow builds from a poisoned cache.

Figure 1. A hash check catches only case 1. Cases 2 and 3 need an independent copy of the expected hash, or analysis of the code itself.

Figure 1 in text. Case 1: the bytes change and the expected hash does not. The check fails. Examples are a corrupted download, a tampered mirror, or a lockfile entry whose hash no longer matches, which npm reports as EINTEGRITY. Case 2: the bytes and the expected hash change together in the same channel. The check passes. Examples are a lockfile edit that rewrites both the resolved URL and the integrity hash, a checksum file hosted on the same server as the download, and a Git tag repointed at a new commit. Case 3: the bytes were malicious before anyone hashed them. The check passes on every later install. Examples are a compromised maintainer publishing a new version, and a release workflow that builds from a poisoned cache. Only case 1 is detected by hashing alone. Cases 2 and 3 need an independent copy of the expected hash, or analysis of the code itself.

Case 1 is what hashing solves. A separate copy of the expected hash catches the swap. The Codecov Bash Uploader compromise ran from January 31 to April 1, 2021, across 108 separate windows, and was discovered when one customer compared the script against the checksum Codecov published on GitHub. The vendor's server served the modified script. GitHub still held the original hash.

Case 2 is a hash and its pointer changing together. We edited a package-lock.json entry labeled ms 2.1.3 so its resolved URL and integrity both pointed at the ms 2.0.0 tarball. npm 12.2.2 ran npm ci without a warning and installed 2.0.0, because the bytes matched the hash they arrived with. The lockfile entry and the integrity check both looked healthy. Our lockfile entry calls this lockfile poisoning. The same pattern explains why a checksum hosted next to its download adds nothing against an attacker who controls that server, and why a repointed tag defeats tag-based pins.

JSON
"node_modules/ms": {
  "version": "2.1.3",
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}

npm's hashes also cover less than people assume. The integrity hash, the registry signature, and package provenance all name the tarball. None covers the registry's copy of the package metadata, which is how manifest confusion works.

Case 3 is the one hashing cannot touch. A compromised maintainer publishes through the normal pipeline, so the registry stores a correct hash, signs it, and serves it to every resolver. The axios 1.14.1 tarball had a valid integrity hash, and lockfiles written during the attack window still pin it. In the TanStack compromise, we warned that an attacker who can run code in GitHub Actions can generate valid Sigstore attestations for malicious packages. What caught axios was content, not cryptography: Socket's automated malware detection flagged its malicious dependency, plain-crypto-js 4.2.1, within six minutes of publication, and our static analysis of its obfuscated postinstall dropper exposed the full payload.

Hashes as malware identifiers

#

Security teams also use hashes in reverse: to name known-bad files. File hashes are a standard indicator of compromise in threat reports. Our Trivy write-up, for example, lists 18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671a as the SHA-256 of the malicious entrypoint.sh. That value lets anyone search their systems, logs, and artifact stores for that exact file.

A file hash also identifies only that exact file. The avalanche property that makes hashes good at detecting tampering makes them useless at detecting variants. One changed byte, a different C2 address, or a rebuilt bundle produces an unrelated hash. Hash blocklists are good for retroactive hunting and bad for prevention, because the next sample never matches the last one.

As of November 2026, the cryptography under most supply chain hashes is sound. SHA-512 and SHA-256 have no practical attacks. The biggest remaining SHA-1 dependency is Git, which mitigates known attacks with collision detection. Git's BreakingChanges document says Git 3.0 will create new repositories with SHA-256 by default, but existing repositories keep SHA-1, and the document says there is no plan to deprecate the SHA-1 format. Every commit pin in a GitHub workflow will be a SHA-1 hash for years.

The unsolved problem is the first hash. Every mechanism in this article records whatever the source served the first time and defends it perfectly afterward. A public log like Go's checksum database makes everyone agree on that first hash, which stops targeted swaps. Nothing makes the first hash honest. Deciding that still means reading the code.