Back to Blog
high SEVERITY4 min read

generateUUID() Ditches MD5 for SHA-256 to Fix CWE-328

The `generateUUID()` helper built request-identifying UUIDs by hashing an input string with MD5, a cryptographically broken algorithm susceptible to collisions. The fix swaps the hash function for SHA-256, reducing the chance that two different inputs produce the same generated identifier.

O
By Orbis AppSec
•Published September 25, 2026•Reviewed September 25, 2026

Answer Summary

The affected code is first-party: the `generateUUID(inputString)` helper used to build `distinct_id` values for outgoing API request headers. An attacker (or even unlucky chance) could exploit MD5's collision weaknesses to make two distinct inputs hash to the same generated UUID, muddying per-request/per-user identification downstream. The fix replaces the MD5 digest with SHA-256 inside `generateUUID()`; no package version applies since this is application code, not a dependency. This maps to CWE-328 (Use of a Broken or Risky Cryptographic Algorithm).

Vulnerability at a Glance

cweCWE-328
fixReplaced the MD5 digest with `crypto.createHash('sha256')`
riskHash collisions in generated distinct_id/UUID values used for request identification
languageJavaScript (Node.js)
root cause`generateUUID()` hashed arbitrary input with `crypto.createHash('md5')`
vulnerabilityUse of a broken cryptographic hash (MD5) for identifier generation

Introduction

generateUUID(inputString) takes an arbitrary string and turns it into a UUID-formatted identifier, used to populate a distinct_id value sent in outgoing API request headers. The problem was in how that string got hashed:

const md5Hash = crypto.createHash('md5')
md5Hash.update(inputString)
const hash = md5Hash.digest('hex')

MD5 is not just "old" — it is cryptographically broken. Practical collision attacks against MD5 have been demonstrated for well over a decade, meaning two different inputs can be crafted (or, less deliberately, can coincidentally occur) to produce the identical 128-bit digest. Since generateUUID() reformats that digest into a UUID-like string, an MD5 collision here means two different inputString values can yield the exact same generated identifier. For code whose entire job is to give each input a distinct, request-identifying value, that's the one property it can't afford to lose.

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem npm (Node.js)
CVE / GHSA not assigned
CWE CWE-328 (Use of a Broken or Risky Cryptographic Algorithm)

There's no package version to pin here — this is application code. The relevant marker is the commit that replaced the md5 digest with sha256 inside generateUUID().

The Vulnerability Explained

The vulnerable pattern is compact:

export function generateUUID(inputString) {
  const md5Hash = crypto.createHash('md5')
  md5Hash.update(inputString)
  const hash = md5Hash.digest('hex')

  // Format the hash into UUID-like string
  const uuid = ...

inputString is hashed with MD5 and the resulting hex digest is sliced into UUID segments. Two issues stack up here:

  1. Collision resistance is broken. MD5 collisions can be engineered with modest compute. If any part of the system lets an attacker influence or choose inputString (for example, a value derived from user-supplied data such as an email address, per how this identifier is populated), a motivated attacker could search for a second input that hashes to the same digest as a target user's, causing the generated distinct_id to collide.
  2. No salt, regardless of algorithm. Even after upgrading the digest algorithm, hashing predictable, low-entropy strings like email addresses without a salt or HMAC key means the output is still amenable to precomputed lookup tables — an attacker who suspects a specific email is registered can hash their guess and compare it against captured identifiers to confirm it.

Attack scenario: imagine a downstream service uses the distinct_id header produced by generateUUID() to route support tickets or attribute telemetry to a user. An attacker who can influence the input to generateUUID() — even indirectly, through a value they control that eventually gets hashed — could attempt to find a colliding input under MD5 that maps to another user's distinct_id. Requests tagged with that collided identifier would then be misattributed, undermining any downstream logic (rate limiting, auditing, support routing) that trusts the identifier's uniqueness.

The Fix

The fix is a one-line algorithm swap inside generateUUID():

const sha256Hash = crypto.createHash('sha256')
sha256Hash.update(inputString)
const hash = sha256Hash.digest('hex')

MD5's createHash('md5') call is replaced with createHash('sha256'). The rest of the function — updating the hash with inputString and formatting the digest into a UUID-like string — is unchanged. SHA-256 has no known practical collision attacks, so two different inputString values are astronomically unlikely to produce the same digest, closing the collision path that made MD5 unsuitable here.

This fix specifically targets the "broken algorithm" half of CWE-328. It does not, on its own, add a salt or HMAC key — if inputString is ever derived from something guessable like an email address, that residual exposure (dictionary/rainbow-table matching against low-entropy input) remains a separate hardening step worth considering.

Key Takeaways

  • generateUUID(inputString) is exactly the kind of "just build an identifier" helper that quietly inherits whatever hash function was copy-pasted into it — check what algorithm sits behind any custom UUID/ID generator in your codebase.
  • MD5's collision weakness matters even for "just a header value" like distinct_id: identifier collisions can misattribute requests, not just break cryptographic signatures.
  • Swapping crypto.createHash('md5') for crypto.createHash('sha256') is a minimal, drop-in fix when the digest length and format don't need to change beyond re-slicing for the UUID template.
  • If the input to a hash-based identifier generator is ever a low-entropy, guessable value (like an email address), upgrading the algorithm alone doesn't add salting — that's a separate control to evaluate.

How Orbis AppSec Detected This

  • Source: the inputString parameter passed into generateUUID()
  • Sink: crypto.createHash('md5') used to derive the identifier's digest
  • Missing control: use of a collision-resistant hash algorithm (and, more broadly, absence of salting for any low-entropy input)
  • CWE: CWE-328 (Use of a Broken or Risky Cryptographic Algorithm)
  • Fix: generateUUID() now hashes inputString with crypto.createHash('sha256') instead of MD5

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

MD5 has no place generating identifiers that need to stay unique, and generateUUID() is a clear example of why: a broken hash function undermines the one guarantee the function exists to provide. The fix — swapping in SHA-256 — is small, but it closes a real collision risk on a code path that feeds directly into request-identifying headers. Anywhere else in your codebase you find createHash('md5') doing similar identifier work deserves the same look.

Prevention and further reading

Frequently Asked Questions

What does `generateUUID(inputString)` actually produce after this fix?

It still returns a UUID-formatted string, but the underlying digest is now computed with SHA-256 instead of MD5 before being sliced into the UUID's dash-separated segments.

Does switching `generateUUID()` from MD5 to SHA-256 make the identifiers safe to derive from email addresses?

It reduces collision risk, but SHA-256 without a salt or HMAC key is still vulnerable to dictionary or rainbow-table attacks against low-entropy inputs like email addresses, so guessable inputs should not be hashed directly for anything security-sensitive.

Is the `distinct_id` header generated by this function used for authentication?

No — per the PR description it's used for API request headers as a tracking-style identifier, not a security token, but consistent uniqueness still matters for accurate request/user attribution.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #138

Related Articles

high

API_URL Defaults to HTTP Without HTTPS Enforcement

An API client defaults to unencrypted HTTP connections, leaving all API communications—including sensitive project data—vulnerable to interception. Although an `upgradeToHttps()` helper function existed, it was applied inconsistently across the codebase. The fix ensures HTTPS enforcement is applied uniformly before every HTTP request.

high

HTTP Client `danger_accept_invalid_certs` Permitted MITM Credential

The HTTP client's `validate_certs` parameter allowed disabling TLS certificate validation through `danger_accept_invalid_certs(true)`, exposing Basic Auth credentials to interception. The fix replaces this dangerous capability with a hard error, forcing developers to use proper certificate management instead.

critical

JWT Authentication Disabled Signature Validation in

A critical misconfiguration in JWT authentication explicitly disabled signature validation, allowing attackers to forge valid tokens with arbitrary claims and bypass authentication entirely. The fix re-enables signature validation on all incoming bearer tokens, restoring the security boundary of the authentication layer.

critical

`requests.get()`/`delete()`/`post()` with `verify=False` in Release

A critical security vulnerability in a release automation script disabled SSL certificate verification on every HTTPS request to GitHub's API. By passing `verify=False` to `requests.get()`, `requests.delete()`, and `requests.post()`, the script exposed OAuth tokens and release binaries to man-in-the-middle attacks on any network the script ran from.

critical

ExternalHttpClient::request() Sent Basic Auth Over Plain HTTP

The `ExternalHttpClient::request()` helper accepted a `$basicAuth` string and passed it straight to the HTTP client's `auth` option without checking that the target URL used `https://`. Any external JSON data source configured with an `http://` endpoint therefore shipped a base64-encoded `Authorization: Basic` header in cleartext on every scheduled load. The fix rejects the request outright — before a client is even created — when the URL scheme is not HTTPS.

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.