fix(core): Reject reserved system column names when creating data tables (#27064)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
Ricardo Espinoza 2026-06-30 06:04:41 -04:00 committed by GitHub
parent 689d873384
commit dbb7fa47c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 62 additions and 5 deletions

View File

@ -100,6 +100,24 @@ describe('POST /projects/:projectId/data-tables', () => {
await authOwnerAgent.post(`/projects/${project.id}/data-tables`).send(payload).expect(400);
});
test('should return 409 when column name conflicts with system columns', async () => {
const project = await createTeamProject(undefined, owner);
for (const systemColumnName of ['id', 'ID', 'createdAt', 'updatedAt']) {
const payload = {
name: `Table with ${systemColumnName}`,
columns: [{ name: systemColumnName, type: 'string' }],
};
const response = await authOwnerAgent
.post(`/projects/${project.id}/data-tables`)
.send(payload)
.expect(409);
expect(response.body.message).toContain('reserved');
}
});
test('should not create data table if user has project:viewer role in team project', async () => {
const project = await createTeamProject(undefined, owner);
await linkUserToProject(member, project, 'project:viewer');

View File

@ -10,6 +10,7 @@ import { DataTableRepository } from '../data-table.repository';
import { DataTableService } from '../data-table.service';
import { mockDataTableSizeValidator } from './test-helpers';
import { DataTableColumnNameConflictError } from '../errors/data-table-column-name-conflict.error';
import { DataTableSystemColumnNameConflictError } from '../errors/data-table-system-column-name-conflict.error';
import { DataTableColumnNotFoundError } from '../errors/data-table-column-not-found.error';
import { DataTableNameConflictError } from '../errors/data-table-name-conflict.error';
import { DataTableNotFoundError } from '../errors/data-table-not-found.error';
@ -140,6 +141,25 @@ describe('dataTable', () => {
expect(project.name).toBe(project1.name);
});
it('should reject columns named after system columns (id, createdAt, updatedAt)', async () => {
for (const systemColumnName of [
'id',
'ID',
'Id',
'createdAt',
'CreatedAt',
'updatedAt',
'UpdatedAt',
]) {
await expect(
dataTableService.createDataTable(project1.id, {
name: `table_with_${systemColumnName}`,
columns: [{ name: systemColumnName, type: 'string' }],
}),
).rejects.toThrow(DataTableSystemColumnNameConflictError);
}
});
it('should return an error if name/project combination already exists', async () => {
const name = 'dataTable';

View File

@ -29,10 +29,11 @@ export class DataTableColumnRepository extends Repository<DataTableColumn> {
* Validates that a column name is not reserved as a system column
*/
private validateNotSystemColumn(columnName: string): void {
if (DATA_TABLE_SYSTEM_COLUMNS.includes(columnName)) {
const lowerName = columnName.toLowerCase();
if (DATA_TABLE_SYSTEM_COLUMNS.some((sc) => sc.toLowerCase() === lowerName)) {
throw new DataTableSystemColumnNameConflictError(columnName);
}
if (columnName === DATA_TABLE_SYSTEM_TESTING_COLUMN) {
if (lowerName === DATA_TABLE_SYSTEM_TESTING_COLUMN.toLowerCase()) {
throw new DataTableSystemColumnNameConflictError(columnName, 'testing');
}
}

View File

@ -112,7 +112,10 @@ export class DataTableController {
} catch (e: unknown) {
if (!(e instanceof Error)) {
throw e;
} else if (e instanceof DataTableNameConflictError) {
} else if (
e instanceof DataTableNameConflictError ||
e instanceof DataTableSystemColumnNameConflictError
) {
throw new ConflictError(e.message);
} else if (e instanceof DataTableValidationError) {
throw new BadRequestError(e.message);

View File

@ -8,7 +8,11 @@ import { GlobalConfig } from '@n8n/config';
import { Project, withTransaction } from '@n8n/db';
import { Service } from '@n8n/di';
import { DataSource, EntityManager, Repository, SelectQueryBuilder } from '@n8n/typeorm';
import { UnexpectedError } from 'n8n-workflow';
import {
DATA_TABLE_SYSTEM_COLUMNS,
DATA_TABLE_SYSTEM_TESTING_COLUMN,
UnexpectedError,
} from 'n8n-workflow';
import type { DataTableInfo, DataTablesSizeData } from 'n8n-workflow';
import { DataTableColumn } from './data-table-column.entity';
@ -16,6 +20,7 @@ import { DataTableDDLService } from './data-table-ddl.service';
import { DataTable } from './data-table.entity';
import { DataTableUserTableName } from './data-table.types';
import { DataTableNameConflictError } from './errors/data-table-name-conflict.error';
import { DataTableSystemColumnNameConflictError } from './errors/data-table-system-column-name-conflict.error';
import { DataTableValidationError } from './errors/data-table-validation.error';
import { isValidColumnName, toTableId, toTableName } from './utils/sql-utils';
@ -56,6 +61,16 @@ export class DataTableRepository extends Repository<DataTable> {
throw new DataTableValidationError(DATA_TABLE_COLUMN_ERROR_MESSAGE);
}
for (const col of columns) {
const lowerName = col.name.toLowerCase();
if (DATA_TABLE_SYSTEM_COLUMNS.some((sc) => sc.toLowerCase() === lowerName)) {
throw new DataTableSystemColumnNameConflictError(col.name);
}
if (lowerName === DATA_TABLE_SYSTEM_TESTING_COLUMN.toLowerCase()) {
throw new DataTableSystemColumnNameConflictError(col.name, 'testing');
}
}
const dataTable = em.create(DataTable, { name, columns, projectId });
await em.insert(DataTable, dataTable);

View File

@ -86,7 +86,7 @@ const getColumnTypeOptions = (compatibleTypes: ColumnType[]) => {
};
const validateColumnName = (columnName: string): string | undefined => {
if (DATA_TABLE_SYSTEM_COLUMNS.includes(columnName)) {
if (DATA_TABLE_SYSTEM_COLUMNS.some((sc) => sc.toLowerCase() === columnName.toLowerCase())) {
return i18n.baseText('dataTable.import.systemColumnName', {
interpolate: { columnName },
});