mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
fix: Revert 'Input validation for workflow and data table names' (PR 30594) (#31359)
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import xss from 'xss';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { scopesSchema } from '../../schemas/scopes.schema';
|
||||
import { xssCheck } from '../../utils/xss-check';
|
||||
import { Z } from '../../zod-class';
|
||||
|
||||
const xssCheck = (value: string) =>
|
||||
value ===
|
||||
xss(value, {
|
||||
whiteList: {},
|
||||
});
|
||||
|
||||
export class UpdateApiKeyRequestDto extends Z.class({
|
||||
label: z.string().max(50).min(1).refine(xssCheck),
|
||||
scopes: scopesSchema,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import xss from 'xss';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { xssCheck } from '../../utils/xss-check';
|
||||
import { Z } from '../../zod-class';
|
||||
|
||||
const xssCheck = (value: string) =>
|
||||
value ===
|
||||
xss(value, {
|
||||
whiteList: {}, // no tags are allowed
|
||||
});
|
||||
|
||||
const URL_REGEX = /^(https?:\/\/|www\.)|(\.[\p{L}\d-]+)/iu;
|
||||
const urlCheck = (value: string) => !URL_REGEX.test(value);
|
||||
|
||||
|
||||
@@ -126,21 +126,6 @@ describe('CreateWorkflowDto', () => {
|
||||
request: { name: 'a'.repeat(129), nodes: [], connections: {} },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name containing a script tag',
|
||||
request: { name: '<script>alert(1)</script>', nodes: [], connections: {} },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name containing an img onerror payload',
|
||||
request: { name: '<img src=x onerror=alert(1)>', nodes: [], connections: {} },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name containing inline HTML markup',
|
||||
request: { name: 'Report <b>bold</b>', nodes: [], connections: {} },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'missing nodes',
|
||||
request: { name: 'Test', connections: {} },
|
||||
|
||||
@@ -98,21 +98,6 @@ describe('UpdateWorkflowDto', () => {
|
||||
request: { name: 'a'.repeat(129) },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name containing a script tag',
|
||||
request: { name: '<script>alert(1)</script>' },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name containing an img onerror payload',
|
||||
request: { name: '<img src=x onerror=alert(1)>' },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name containing inline HTML markup',
|
||||
request: { name: 'Report <b>bold</b>' },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'invalid nodes type',
|
||||
request: { nodes: 'not-an-array' },
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { IPinData, IConnections, IDataObject, INode, IWorkflowSettings } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { xssCheck } from '../../utils/xss-check';
|
||||
|
||||
export const WORKFLOW_NAME_MAX_LENGTH = 128;
|
||||
|
||||
/** Maximum allowed size for pinned data in bytes (12 MB) */
|
||||
@@ -19,8 +17,7 @@ export const workflowNameSchema = z
|
||||
.min(1, { message: 'Workflow name is required' })
|
||||
.max(WORKFLOW_NAME_MAX_LENGTH, {
|
||||
message: `Workflow name must be ${WORKFLOW_NAME_MAX_LENGTH} characters or less`,
|
||||
})
|
||||
.refine(xssCheck, { message: 'Potentially malicious string' });
|
||||
});
|
||||
|
||||
export const workflowDescriptionSchema = z.string().nullable();
|
||||
|
||||
|
||||
@@ -457,7 +457,6 @@ export {
|
||||
} from './schemas/eval-insights.schema';
|
||||
|
||||
export { ALLOWED_DOMAINS, isAllowedDomain } from './utils/allowed-domains';
|
||||
export { xssCheck } from './utils/xss-check';
|
||||
|
||||
export type { PublishTimelineEvent } from './schemas/workflow-publish-timeline.schema';
|
||||
export {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { dataTableNameSchema } from '../data-table.schema';
|
||||
|
||||
describe('dataTableNameSchema', () => {
|
||||
describe('Valid names', () => {
|
||||
test.each([
|
||||
'Customers',
|
||||
'Customer orders 2024',
|
||||
'orders-q1',
|
||||
"Q1 'Quarterly' Report",
|
||||
'orders & invoices',
|
||||
'a',
|
||||
])('accepts %p', (value) => {
|
||||
expect(dataTableNameSchema.safeParse(value).success).toBe(true);
|
||||
});
|
||||
|
||||
test('trims surrounding whitespace', () => {
|
||||
const result = dataTableNameSchema.safeParse(' Customers ');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBe('Customers');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid names', () => {
|
||||
test.each([
|
||||
['empty string', ''],
|
||||
['only whitespace', ' '],
|
||||
['too long', 'a'.repeat(129)],
|
||||
['contains a script tag', '<script>alert(1)</script>'],
|
||||
['contains an img onerror payload', '<img src=x onerror=alert(1)>'],
|
||||
['contains inline HTML markup', 'Customers <b>bold</b>'],
|
||||
['contains an svg onload payload', '<svg onload=alert(1)>'],
|
||||
])('rejects %s', (_label, value) => {
|
||||
expect(dataTableNameSchema.safeParse(value).success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { ListDataTableQueryDto } from '../dto';
|
||||
import { xssCheck } from '../utils/xss-check';
|
||||
|
||||
export const insertRowReturnType = z.union([z.literal('all'), z.literal('count'), z.literal('id')]);
|
||||
|
||||
export const dataTableNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.refine(xssCheck, { message: 'Potentially malicious string' });
|
||||
export const dataTableNameSchema = z.string().trim().min(1).max(128);
|
||||
export const dataTableIdSchema = z
|
||||
.string()
|
||||
.max(36)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { xssCheck } from '../xss-check';
|
||||
|
||||
describe('xssCheck', () => {
|
||||
test.each([
|
||||
'My Workflow',
|
||||
'My Workflow 2024',
|
||||
'Workflow with spaces and 123 numbers',
|
||||
"O'Brien's workflow",
|
||||
'workflow & report',
|
||||
'a',
|
||||
'name-with-dashes_and.dots',
|
||||
'name/with/slashes',
|
||||
'name (with) (parens)',
|
||||
])('returns true for plain string %p', (value) => {
|
||||
expect(xssCheck(value)).toBe(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
'<script>alert(1)</script>',
|
||||
'<img src=x onerror=alert(1)>',
|
||||
'<svg onload=alert(1)>',
|
||||
'<a href="javascript:alert(1)">click</a>',
|
||||
'<iframe src="evil"></iframe>',
|
||||
'Name with <b>bold</b>',
|
||||
'<style>body{}</style>',
|
||||
'<SCRIPT>alert(1)</SCRIPT>',
|
||||
'<script src=//evil.com></script>',
|
||||
'7 > 3 is true',
|
||||
'< not really a tag',
|
||||
])('returns false for value containing HTML-significant characters %p', (value) => {
|
||||
expect(xssCheck(value)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for empty string', () => {
|
||||
expect(xssCheck('')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import xss from 'xss';
|
||||
|
||||
/**
|
||||
* Returns `true` when the value is preserved by `xss({ whiteList: {} })`,
|
||||
* i.e. contains no HTML-significant characters.
|
||||
*
|
||||
* Use as a zod refine guard for user-supplied names (workflows, data tables,
|
||||
* user names, API keys, etc.).
|
||||
*/
|
||||
export const xssCheck = (value: string): boolean => value === xss(value, { whiteList: {} });
|
||||
@@ -24,6 +24,7 @@ import { objectRetriever, sqlite } from '../utils/transformers';
|
||||
|
||||
@Entity()
|
||||
export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkflowDb {
|
||||
// TODO: Add XSS check
|
||||
@Index({ unique: true })
|
||||
@Length(1, 128, {
|
||||
message: 'Workflow name must be $constraint1 to $constraint2 characters long.',
|
||||
|
||||
Reference in New Issue
Block a user