From ca04510db18a20fee32e9d6e974d21f63e2c6960 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 14:42:22 +0000 Subject: [PATCH 1/4] Guard object lookups against inherited prototype properties When user-controlled candidate values like "constructor" are used as keys to look up values in plain objects (staticValues, plugin values, modifiers, config), they can match inherited Object.prototype properties instead of returning undefined. This caused crashes like "V.map is not a function" when scanning source files containing strings like "row-constructor". Use Object.hasOwn() checks before all user-keyed object lookups in: - utilities.ts (staticValues lookup) - plugin-api.ts (values, modifiers, and variant values lookups) - plugin-functions.ts (get() config traversal function) Fixes #19721 https://claude.ai/code/session_011CYSGw3DLh2Z8xnuyoaCgC --- .../tailwindcss/src/compat/plugin-api.test.ts | 44 +++++++++++++++++++ packages/tailwindcss/src/compat/plugin-api.ts | 24 +++++++--- .../src/compat/plugin-functions.ts | 11 ++--- packages/tailwindcss/src/utilities.test.ts | 7 +++ packages/tailwindcss/src/utilities.ts | 6 ++- 5 files changed, 77 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 4e35afa5a84e..cc410995b442 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -4592,4 +4592,48 @@ describe('config()', () => { expect(fn).toHaveBeenCalledWith('defaultvalue') }) + + // https://github.com/tailwindlabs/tailwindcss/issues/19721 + test('matchUtilities does not match Object.prototype properties as values', async ({ + expect, + }) => { + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ matchUtilities }) { + matchUtilities( + { + test: (value) => ({ '--test': value }), + }, + { + values: { + foo: 'bar', + }, + }, + ) + }), + } + }, + }) + + // These should not crash or produce output + expect( + optimizeCss( + compiler.build([ + 'test-constructor', + 'test-hasOwnProperty', + 'test-toString', + 'test-valueOf', + 'test-__proto__', + ]), + ).trim(), + ).toEqual('') + }) }) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 5954040017d2..e4b2d2b71d66 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -202,6 +202,9 @@ export function buildPluginApi({ ruleNodes.nodes, ) } else if (variant.value.kind === 'named' && options?.values) { + if (!Object.hasOwn(options.values, variant.value.value)) { + return null + } let defaultValue = options.values[variant.value.value] if (typeof defaultValue !== 'string') { return null @@ -223,8 +226,14 @@ export function buildPluginApi({ let aValueKey = a.value ? a.value.value : 'DEFAULT' let zValueKey = z.value ? z.value.value : 'DEFAULT' - let aValue = options?.values?.[aValueKey] ?? aValueKey - let zValue = options?.values?.[zValueKey] ?? zValueKey + let aValue = + (options?.values && Object.hasOwn(options.values, aValueKey) + ? options.values[aValueKey] + : undefined) ?? aValueKey + let zValue = + (options?.values && Object.hasOwn(options.values, zValueKey) + ? options.values[zValueKey] + : undefined) ?? zValueKey if (options && typeof options.sort === 'function') { return options.sort( @@ -406,12 +415,15 @@ export function buildPluginApi({ value = values.DEFAULT ?? null } else if (candidate.value.kind === 'arbitrary') { value = candidate.value.value - } else if (candidate.value.fraction && values[candidate.value.fraction]) { + } else if ( + candidate.value.fraction && + Object.hasOwn(values, candidate.value.fraction) + ) { value = values[candidate.value.fraction] ignoreModifier = true - } else if (values[candidate.value.value]) { + } else if (Object.hasOwn(values, candidate.value.value)) { value = values[candidate.value.value] - } else if (values.__BARE_VALUE__) { + } else if (Object.hasOwn(values, '__BARE_VALUE__')) { value = values.__BARE_VALUE__(candidate.value) ?? null ignoreModifier = (candidate.value.fraction !== null && value?.includes('/')) ?? false @@ -430,7 +442,7 @@ export function buildPluginApi({ modifier = null } else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') { modifier = candidate.modifier.value - } else if (modifiers?.[candidate.modifier.value]) { + } else if (modifiers && Object.hasOwn(modifiers, candidate.modifier.value)) { modifier = modifiers[candidate.modifier.value] } else if (isColor && !Number.isNaN(Number(candidate.modifier.value))) { modifier = `${candidate.modifier.value}%` diff --git a/packages/tailwindcss/src/compat/plugin-functions.ts b/packages/tailwindcss/src/compat/plugin-functions.ts index f311ad1e8c43..40b8c93e89f1 100644 --- a/packages/tailwindcss/src/compat/plugin-functions.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -223,8 +223,10 @@ function get(obj: any, path: string[]) { for (let i = 0; i < path.length; ++i) { let key = path[i] - // The key does not exist so concatenate it with the next key - if (obj?.[key] === undefined) { + // The key does not exist so concatenate it with the next key. + // We use Object.hasOwn to avoid matching inherited prototype properties + // (e.g. "constructor", "toString") when traversing config objects. + if (obj === null || obj === undefined || typeof obj !== 'object' || !Object.hasOwn(obj, key)) { if (path[i + 1] === undefined) { return undefined } @@ -233,11 +235,6 @@ function get(obj: any, path: string[]) { continue } - // We never want to index into strings - if (typeof obj === 'string') { - return undefined - } - obj = obj[key] } diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 360acd9e29a6..e562cd3be15a 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -1646,6 +1646,13 @@ test('row', async () => { 'row-span-full/foo', 'row-[span_123/span_123]/foo', 'row-span-[var(--my-variable)]/foo', + + // Candidates matching Object.prototype properties should not crash or + // produce output (see: https://github.com/tailwindlabs/tailwindcss/issues/19721) + 'row-constructor', + 'row-hasOwnProperty', + 'row-toString', + 'row-valueOf', ]), ).toEqual('') diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index aa2187887405..0beeea8033ea 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -439,8 +439,10 @@ export function createUtilities(theme: Theme) { } if (value === null && !negative && desc.staticValues && !candidate.modifier) { - let fallback = desc.staticValues[candidate.value.value] - if (fallback) return fallback.map(cloneAstNode) + if (Object.hasOwn(desc.staticValues, candidate.value.value)) { + let fallback = desc.staticValues[candidate.value.value] + if (fallback) return fallback.map(cloneAstNode) + } } } From e35633b262cff03f90b7f1a9f355cb248bff909a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 25 Feb 2026 15:54:19 +0100 Subject: [PATCH 2/4] handle prototype pollution ahead of time Otherwise the `Object.hasOwn` check runs for every utility where static values have been defined and uses bare values (e.g. `row-constructor`) but in most cases you won't hit prototype specific keys. This takes it out of the hot path --- packages/tailwindcss/src/utilities.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 0beeea8033ea..62e4510b7309 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -391,6 +391,8 @@ export function createUtilities(theme: Theme) { * user's theme. */ function functionalUtility(classRoot: string, desc: UtilityDescription) { + if (desc.staticValues) desc.staticValues = Object.assign(Object.create(null), desc.staticValues) + function handleFunctionalUtility({ negative }: { negative: boolean }) { return (candidate: Extract) => { let value: string | null = null @@ -439,10 +441,8 @@ export function createUtilities(theme: Theme) { } if (value === null && !negative && desc.staticValues && !candidate.modifier) { - if (Object.hasOwn(desc.staticValues, candidate.value.value)) { - let fallback = desc.staticValues[candidate.value.value] - if (fallback) return fallback.map(cloneAstNode) - } + let fallback = desc.staticValues[candidate.value.value] + if (fallback) return fallback.map(cloneAstNode) } } From 3c76cb80796a93512123dc9d5dc25f05ad7b681c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 25 Feb 2026 16:01:46 +0100 Subject: [PATCH 3/4] __BARE_VALUE__ is a safe value to use --- packages/tailwindcss/src/compat/plugin-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index e4b2d2b71d66..af6e98d770ff 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -423,7 +423,7 @@ export function buildPluginApi({ ignoreModifier = true } else if (Object.hasOwn(values, candidate.value.value)) { value = values[candidate.value.value] - } else if (Object.hasOwn(values, '__BARE_VALUE__')) { + } else if (values.__BARE_VALUE__) { value = values.__BARE_VALUE__(candidate.value) ?? null ignoreModifier = (candidate.value.fraction !== null && value?.includes('/')) ?? false From c2b9cffd707ec873e95780b7699c5cf220547859 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 25 Feb 2026 16:11:08 +0100 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2359f1812b0..96297b4d449d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) +### Fixed + +- Guard object lookups against inherited prototype properties ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725)) + ## [4.2.1] - 2026-02-23 ### Fixed