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 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..af6e98d770ff 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,10 +415,13 @@ 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__) { value = values.__BARE_VALUE__(candidate.value) ?? null @@ -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..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