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:
parent
097f982d7a
commit
9ded4a23de
6 changed files with 78 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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}%`
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue