Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
})
})
22 changes: 17 additions & 5 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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}%`
Expand Down
11 changes: 4 additions & 7 deletions packages/tailwindcss/src/compat/plugin-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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]
}

Expand Down
7 changes: 7 additions & 0 deletions packages/tailwindcss/src/utilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('')

Expand Down
2 changes: 2 additions & 0 deletions packages/tailwindcss/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down