Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/proud-pets-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/table-core': patch
---

fix prototype-named column ids (e.g. `hasOwnProperty`, `toString`) crashing filtering/sorting

Row value caches (`_valuesCache`, `_uniqueValuesCache`, `_groupingValuesCache`) were plain objects probed with `cache.hasOwnProperty(columnId)`. A column whose id collided with an `Object.prototype` member shadowed the inherited method on write, so the next probe invoked a cached value as a function and threw `TypeError`. Caches are now created with `Object.create(null)` and read via `Object.prototype.hasOwnProperty.call`, and `_getAllCellsByColumnId` is likewise null-prototyped so prototype-named ids resolve correctly.
12 changes: 7 additions & 5 deletions packages/table-core/src/core/row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ export const createRow = <TData extends RowData>(
original,
depth,
parentId,
_valuesCache: {},
_uniqueValuesCache: {},
_valuesCache: Object.create(null),
_uniqueValuesCache: Object.create(null),
getValue: (columnId) => {
if (row._valuesCache.hasOwnProperty(columnId)) {
if (Object.prototype.hasOwnProperty.call(row._valuesCache, columnId)) {
return row._valuesCache[columnId]
}

Expand All @@ -128,7 +128,9 @@ export const createRow = <TData extends RowData>(
return row._valuesCache[columnId] as any
},
getUniqueValues: (columnId) => {
if (row._uniqueValuesCache.hasOwnProperty(columnId)) {
if (
Object.prototype.hasOwnProperty.call(row._uniqueValuesCache, columnId)
) {
return row._uniqueValuesCache[columnId]
}

Expand Down Expand Up @@ -185,7 +187,7 @@ export const createRow = <TData extends RowData>(
acc[cell.column.id] = cell
return acc
},
{} as Record<string, Cell<TData, unknown>>,
Object.create(null) as Record<string, Cell<TData, unknown>>,
)
},
getMemoOptions(table.options, 'debugRows', 'getAllCellsByColumnId'),
Expand Down
9 changes: 7 additions & 2 deletions packages/table-core/src/features/ColumnGrouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,12 @@ export const ColumnGrouping: TableFeature = {
): void => {
row.getIsGrouped = () => !!row.groupingColumnId
row.getGroupingValue = (columnId) => {
if (row._groupingValuesCache.hasOwnProperty(columnId)) {
if (
Object.prototype.hasOwnProperty.call(
row._groupingValuesCache,
columnId,
)
) {
return row._groupingValuesCache[columnId]
}

Expand All @@ -379,7 +384,7 @@ export const ColumnGrouping: TableFeature = {

return row._groupingValuesCache[columnId]
}
row._groupingValuesCache = {}
row._groupingValuesCache = Object.create(null)
},

createCell: <TData extends RowData, TValue>(
Expand Down
14 changes: 12 additions & 2 deletions packages/table-core/src/utils/getGroupedRowModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ export function getGroupedRowModel<TData extends RowData>(): (
getValue: (columnId: string) => {
// Don't aggregate columns that are in the grouping
if (existingGrouping.includes(columnId)) {
if (row._valuesCache.hasOwnProperty(columnId)) {
if (
Object.prototype.hasOwnProperty.call(
row._valuesCache,
columnId,
)
) {
return row._valuesCache[columnId]
}

Expand All @@ -106,7 +111,12 @@ export function getGroupedRowModel<TData extends RowData>(): (
return row._valuesCache[columnId]
}

if (row._groupingValuesCache.hasOwnProperty(columnId)) {
if (
Object.prototype.hasOwnProperty.call(
row._groupingValuesCache,
columnId,
)
) {
return row._groupingValuesCache[columnId]
}

Expand Down
78 changes: 78 additions & 0 deletions packages/table-core/tests/prototypeColumnId.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from 'vitest'
import { ColumnDef, getCoreRowModel } from '../src'
import { createTable } from '../src/core/table'

type Row = Record<string, string>

// Column ids that collide with Object.prototype members used to poison the
// per-row value caches (see Row.getValue / _valuesCache). These must be
// treated as plain data keys, not as inherited methods.
const PROTOTYPE_IDS = [
'hasOwnProperty',
'toString',
'constructor',
'valueOf',
'__proto__',
'isPrototypeOf',
]

function makeTable(data: Row[]) {
const columns: ColumnDef<Row>[] = PROTOTYPE_IDS.map((id) => ({
id,
accessorFn: (row) => row[id],
}))

return createTable<Row>({
onStateChange() {},
renderFallbackValue: '',
data,
state: {},
columns,
getCoreRowModel: getCoreRowModel(),
})
}

describe('prototype-named column ids', () => {
it('does not throw and returns the correct value from getValue', () => {
const data: Row[] = [
Object.fromEntries(PROTOTYPE_IDS.map((id) => [id, `${id}-value`])),
]
const table = makeTable(data)
const row = table.getCoreRowModel().rows[0]!

for (const id of PROTOTYPE_IDS) {
// First read populates the cache, the second reads it back. Neither
// should throw, and both should yield the user-supplied value.
expect(() => row.getValue(id)).not.toThrow()
expect(row.getValue(id)).toBe(`${id}-value`)
expect(row.getValue(id)).toBe(`${id}-value`)
}
})

it('does not let a cached hasOwnProperty value break later getValue calls', () => {
const data: Row[] = [
Object.fromEntries(PROTOTYPE_IDS.map((id) => [id, `${id}-value`])),
]
const table = makeTable(data)
const row = table.getCoreRowModel().rows[0]!

// Reading the hasOwnProperty column used to overwrite the inherited method
// on the cache, breaking every subsequent getValue on the row.
expect(row.getValue('hasOwnProperty')).toBe('hasOwnProperty-value')
expect(() => row.getValue('toString')).not.toThrow()
expect(row.getValue('toString')).toBe('toString-value')
})

it('returns unique values for prototype-named column ids', () => {
const data: Row[] = [
Object.fromEntries(PROTOTYPE_IDS.map((id) => [id, `${id}-value`])),
]
const table = makeTable(data)
const row = table.getCoreRowModel().rows[0]!

for (const id of PROTOTYPE_IDS) {
expect(() => row.getUniqueValues(id)).not.toThrow()
expect(row.getUniqueValues(id)).toEqual([`${id}-value`])
}
})
})