Safeguard
Vulnerability Analysis

CVE-2018-3728: Prototype pollution in hoek

CVE-2018-3728 is a prototype pollution flaw in Hoek's merge functions, exposing hapi.js and Joi-based apps to __proto__ injection. Here's the impact, fix, and remediation path.

Nayan Dey
Security Researcher
7 min read

Prototype pollution has become one of the most persistent vulnerability classes in the Node.js ecosystem, and CVE-2018-3728 is one of the foundational cases that put it on the map. The flaw sits in Hoek, the general-purpose utility library built and maintained by the hapi.js team, which sees enormous indirect exposure because it underpins Joi (schema validation) and hapi itself, and by extension thousands of applications that never directly require('hoek') but pull it in three or four dependency layers deep.

The vulnerability lives in Hoek's object-merging helpers, merge() and applyToDefaults(). These functions recursively copy properties from a source object onto a target object — a common pattern for combining user-supplied configuration with application defaults. The vulnerable implementations did not exclude the special __proto__ key from that copy operation. If an attacker can control the shape of an object that gets passed into merge() or applyToDefaults() — for example, a JSON request body that is later merged with default options — they can include a __proto__ property whose nested keys get written directly onto Object.prototype. Because every plain object in a JavaScript process inherits from Object.prototype, that single write pollutes the entire runtime: previously undefined properties suddenly appear as truthy values on unrelated objects throughout the application. Depending on what the application does with those inherited properties afterward, the consequences range from denial of service (crashes from unexpected property types) to authentication or authorization logic being silently bypassed, to more severe outcomes if a polluted property is later consumed by something that executes code or shells out.

Affected versions and components

CVE-2018-3728 affects Hoek releases prior to the fixes shipped in the 4.x and 5.x lines: versions before 4.2.1 and before 5.0.3 contain the unguarded merge logic. The hapi.js team addressed the issue by stripping __proto__ handling out of the merge path in those patched releases.

The practical impact, however, extends well past anyone who lists hoek directly in package.json. Hoek is a core dependency of Joi and hapi, and at the time of disclosure a large number of hapi-based services and Joi-validated APIs had vulnerable Hoek versions locked in via semver ranges in their own dependency trees. That meant teams could be exposed to CVE-2018-3728 without ever having heard of Hoek, simply by running an application built on hapi or using Joi for input validation. This transitive-exposure pattern is exactly what makes prototype pollution bugs in low-level utility libraries so disproportionately impactful compared to their line-of-code footprint.

CVSS, EPSS, and KEV context

NVD catalogs CVE-2018-3728 as a medium-severity issue: the attack vector is network-reachable and requires no authentication or special privileges, but real-world exploitability depends heavily on whether a specific application actually funnels attacker-controlled input into a vulnerable merge() or applyToDefaults() call and then does something security-relevant with the resulting object. That conditional exploitability is typical of prototype pollution findings — the library bug is a primitive, not a ready-made exploit, and the actual severity for any given deployment depends on the surrounding application logic.

The vulnerability does not appear in CISA's Known Exploited Vulnerabilities (KEV) catalog, and its exploitation-probability signals are consistent with what's typical for library-level prototype pollution: low observed in-the-wild exploitation relative to its theoretical reach, since attackers generally need to chain the pollution primitive with an application-specific "gadget" to turn it into something more concrete than a crash. That said, low observed exploitation is not the same as low risk — prototype pollution primitives are frequently rediscovered and chained into new exploits years after the original disclosure, and Hoek's central position in the hapi ecosystem means the population of theoretically reachable applications was, and in unpatched deployments still is, substantial.

Timeline

  • Root cause: Hoek's merge() and applyToDefaults() functions performed deep, recursive property copying without excluding __proto__, a pattern that predates the vulnerability's public identification by some time — it was simply how the utility had always worked.
  • Disclosure: CVE-2018-3728 was identified and reported as part of the broader wave of prototype-pollution research into popular npm packages that security researchers, including Snyk's security research team, conducted in 2018, examining how "merge" and "extend"-style utilities across the npm ecosystem handled untrusted input.
  • Fix shipped: The hapi.js maintainers released patched versions, 4.2.1 and 5.0.3, which strip or reject __proto__ during merge operations, closing the pollution path.
  • CVE assignment and cataloging: The issue was assigned CVE-2018-3728 and published in the National Vulnerability Database, with corresponding advisories in the npm and GitHub Advisory Database ecosystems, flagging vulnerable version ranges for automated tooling.
  • Downstream remediation tail: Because hapi and Joi both depended on Hoek, those packages needed their own releases bumping their internal Hoek version constraints before consuming applications could get a clean dependency tree simply by running npm update. This extended the effective remediation window for many consumers well beyond Hoek's own patch date, since teams pinned to older hapi or Joi majors had to upgrade those frameworks — not just the transitive Hoek dependency — to fully resolve the issue.

Remediation steps

  1. Upgrade Hoek directly to 4.2.1 or later (for the 4.x line) or 5.0.3 or later (for the 5.x line), or migrate to a current major version altogether.
  2. Audit transitive dependencies. Run npm ls hoek or yarn why hoek across your services to find where the package is pulled in via hapi, Joi, or other hapi-ecosystem packages. If it's transitive, you may also need to upgrade the parent package (hapi, Joi) to a release that itself requires a patched Hoek version, since older parent packages may pin vulnerable ranges regardless of what's available on npm.
  3. Regenerate and pin lockfiles (package-lock.json / yarn.lock) after upgrading, and verify the resolved Hoek version in the lockfile rather than trusting the top-level package.json range.
  4. Add automated dependency scanning to CI/CD. npm audit, yarn audit, or a dedicated software composition analysis (SCA) tool should run on every build and pull request, failing on known-vulnerable prototype pollution advisories rather than relying on periodic manual review.
  5. Apply defense-in-depth for prototype pollution generally, since new instances of this bug class continue to surface: avoid merging untrusted, attacker-controlled objects directly into application state or configuration; explicitly reject or strip __proto__, constructor, and prototype keys from any object built from external input; prefer Map over plain objects when storing user-supplied key/value data; and where compatible with your runtime, consider Object.freeze(Object.prototype) as a hardening measure in security-sensitive processes.
  6. Re-scan after remediation to confirm no other code path — including test fixtures, build tooling, or internal scripts — still resolves a vulnerable Hoek version.

How Safeguard Helps

CVE-2018-3728 is a textbook case for why software supply chain visibility has to go past package.json and into the actual resolved dependency graph. Safeguard's software composition analysis continuously builds a full SBOM for your services, mapping not just direct dependencies but the transitive chain — so a vulnerable Hoek version hiding three layers under hapi or Joi is surfaced with the exact path that pulled it in, rather than requiring an engineer to manually trace npm ls output across dozens of services.

Safeguard cross-references your resolved dependency versions against NVD, the GitHub Advisory Database, and npm security advisories in near real time, automatically flagging any component matching CVE-2018-3728's vulnerable ranges (Hoek before 4.2.1 or before 5.0.3) the moment it enters a lockfile — whether that's a fresh install or a reintroduction from a downgraded parent package. Policy gates can be configured to block merges or builds that would ship a known-vulnerable Hoek version into production, closing the gap where a patched top-level dependency silently reverts after a routine npm install.

Because prototype pollution risk compounds with how deeply a vulnerable utility is embedded in a framework, Safeguard's dependency graph view also helps teams prioritize remediation: distinguishing between a directly-declared vulnerable dependency and one buried under a widely used framework like hapi, so engineering time goes toward the fixes with the broadest blast radius first. Combined with continuous monitoring for newly disclosed advisories affecting your existing dependency tree, this turns a vulnerability class that has historically resurfaced for years after initial disclosure into something your team can detect and remediate before it becomes an incident.

Never miss an update

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