Guard object lookups against inherited prototype properties (#19725)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Adam Wathan 2026-02-25 10:16:09 -05:00 • committed by GitHub
parent 097f982d7a
commit 9ded4a23de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 78 additions and 12 deletions

View file

@ -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

View file

@ -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('')
})
})

View file

@ -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}%`

View file

@ -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]
}

View file

@ -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('')

View file

@ -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<Candidate, { kind: 'functional' }>) => {
let value: string | null = null