Skip to content

Commit 9ded4a2

Browse files
adamwathanclaudeRobinMalfait
authored
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>
1 parent 097f982 commit 9ded4a2

File tree

6 files changed

+78
-12
lines changed

6 files changed

+78
-12
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
1313

14+
### Fixed
15+
16+
- Guard object lookups against inherited prototype properties ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
17+
1418
## [4.2.1] - 2026-02-23
1519

1620
### Fixed

packages/tailwindcss/src/compat/plugin-api.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4592,4 +4592,48 @@ describe('config()', () => {
45924592

45934593
expect(fn).toHaveBeenCalledWith('defaultvalue')
45944594
})
4595+
4596+
// https://github.com/tailwindlabs/tailwindcss/issues/19721
4597+
test('matchUtilities does not match Object.prototype properties as values', async ({
4598+
expect,
4599+
}) => {
4600+
let input = css`
4601+
@tailwind utilities;
4602+
@plugin "my-plugin";
4603+
`
4604+
4605+
let compiler = await compile(input, {
4606+
loadModule: async (id, base) => {
4607+
return {
4608+
path: '',
4609+
base,
4610+
module: plugin(function ({ matchUtilities }) {
4611+
matchUtilities(
4612+
{
4613+
test: (value) => ({ '--test': value }),
4614+
},
4615+
{
4616+
values: {
4617+
foo: 'bar',
4618+
},
4619+
},
4620+
)
4621+
}),
4622+
}
4623+
},
4624+
})
4625+
4626+
// These should not crash or produce output
4627+
expect(
4628+
optimizeCss(
4629+
compiler.build([
4630+
'test-constructor',
4631+
'test-hasOwnProperty',
4632+
'test-toString',
4633+
'test-valueOf',
4634+
'test-__proto__',
4635+
]),
4636+
).trim(),
4637+
).toEqual('')
4638+
})
45954639
})

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ export function buildPluginApi({
202202
ruleNodes.nodes,
203203
)
204204
} else if (variant.value.kind === 'named' && options?.values) {
205+
if (!Object.hasOwn(options.values, variant.value.value)) {
206+
return null
207+
}
205208
let defaultValue = options.values[variant.value.value]
206209
if (typeof defaultValue !== 'string') {
207210
return null
@@ -223,8 +226,14 @@ export function buildPluginApi({
223226
let aValueKey = a.value ? a.value.value : 'DEFAULT'
224227
let zValueKey = z.value ? z.value.value : 'DEFAULT'
225228

226-
let aValue = options?.values?.[aValueKey] ?? aValueKey
227-
let zValue = options?.values?.[zValueKey] ?? zValueKey
229+
let aValue =
230+
(options?.values && Object.hasOwn(options.values, aValueKey)
231+
? options.values[aValueKey]
232+
: undefined) ?? aValueKey
233+
let zValue =
234+
(options?.values && Object.hasOwn(options.values, zValueKey)
235+
? options.values[zValueKey]
236+
: undefined) ?? zValueKey
228237

229238
if (options && typeof options.sort === 'function') {
230239
return options.sort(
@@ -406,10 +415,13 @@ export function buildPluginApi({
406415
value = values.DEFAULT ?? null
407416
} else if (candidate.value.kind === 'arbitrary') {
408417
value = candidate.value.value
409-
} else if (candidate.value.fraction && values[candidate.value.fraction]) {
418+
} else if (
419+
candidate.value.fraction &&
420+
Object.hasOwn(values, candidate.value.fraction)
421+
) {
410422
value = values[candidate.value.fraction]
411423
ignoreModifier = true
412-
} else if (values[candidate.value.value]) {
424+
} else if (Object.hasOwn(values, candidate.value.value)) {
413425
value = values[candidate.value.value]
414426
} else if (values.__BARE_VALUE__) {
415427
value = values.__BARE_VALUE__(candidate.value) ?? null
@@ -430,7 +442,7 @@ export function buildPluginApi({
430442
modifier = null
431443
} else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') {
432444
modifier = candidate.modifier.value
433-
} else if (modifiers?.[candidate.modifier.value]) {
445+
} else if (modifiers && Object.hasOwn(modifiers, candidate.modifier.value)) {
434446
modifier = modifiers[candidate.modifier.value]
435447
} else if (isColor && !Number.isNaN(Number(candidate.modifier.value))) {
436448
modifier = `${candidate.modifier.value}%`

packages/tailwindcss/src/compat/plugin-functions.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,10 @@ function get(obj: any, path: string[]) {
223223
for (let i = 0; i < path.length; ++i) {
224224
let key = path[i]
225225

226-
// The key does not exist so concatenate it with the next key
227-
if (obj?.[key] === undefined) {
226+
// The key does not exist so concatenate it with the next key.
227+
// We use Object.hasOwn to avoid matching inherited prototype properties
228+
// (e.g. "constructor", "toString") when traversing config objects.
229+
if (obj === null || obj === undefined || typeof obj !== 'object' || !Object.hasOwn(obj, key)) {
228230
if (path[i + 1] === undefined) {
229231
return undefined
230232
}
@@ -233,11 +235,6 @@ function get(obj: any, path: string[]) {
233235
continue
234236
}
235237

236-
// We never want to index into strings
237-
if (typeof obj === 'string') {
238-
return undefined
239-
}
240-
241238
obj = obj[key]
242239
}
243240

packages/tailwindcss/src/utilities.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,13 @@ test('row', async () => {
16461646
'row-span-full/foo',
16471647
'row-[span_123/span_123]/foo',
16481648
'row-span-[var(--my-variable)]/foo',
1649+
1650+
// Candidates matching Object.prototype properties should not crash or
1651+
// produce output (see: https://github.com/tailwindlabs/tailwindcss/issues/19721)
1652+
'row-constructor',
1653+
'row-hasOwnProperty',
1654+
'row-toString',
1655+
'row-valueOf',
16491656
]),
16501657
).toEqual('')
16511658

packages/tailwindcss/src/utilities.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,8 @@ export function createUtilities(theme: Theme) {
391391
* user's theme.
392392
*/
393393
function functionalUtility(classRoot: string, desc: UtilityDescription) {
394+
if (desc.staticValues) desc.staticValues = Object.assign(Object.create(null), desc.staticValues)
395+
394396
function handleFunctionalUtility({ negative }: { negative: boolean }) {
395397
return (candidate: Extract<Candidate, { kind: 'functional' }>) => {
396398
let value: string | null = null

0 commit comments

Comments
 (0)