diff --git a/packages/@n8n/api-types/src/dto/api-keys/update-api-key-request.dto.ts b/packages/@n8n/api-types/src/dto/api-keys/update-api-key-request.dto.ts
index 31b52659564..b123b718ed1 100644
--- a/packages/@n8n/api-types/src/dto/api-keys/update-api-key-request.dto.ts
+++ b/packages/@n8n/api-types/src/dto/api-keys/update-api-key-request.dto.ts
@@ -1,15 +1,9 @@
-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,
diff --git a/packages/@n8n/api-types/src/dto/user/user-update-request.dto.ts b/packages/@n8n/api-types/src/dto/user/user-update-request.dto.ts
index abc044a8f42..776dac21563 100644
--- a/packages/@n8n/api-types/src/dto/user/user-update-request.dto.ts
+++ b/packages/@n8n/api-types/src/dto/user/user-update-request.dto.ts
@@ -1,14 +1,8 @@
-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);
diff --git a/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow.dto.test.ts b/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow.dto.test.ts
index da04c53152f..52dfa0368d9 100644
--- a/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow.dto.test.ts
+++ b/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow.dto.test.ts
@@ -108,6 +108,21 @@ describe('CreateWorkflowDto', () => {
request: { name: 'a'.repeat(129), nodes: [], connections: {} },
expectedErrorPath: ['name'],
},
+ {
+ name: 'name containing a script tag',
+ request: { name: '', nodes: [], connections: {} },
+ expectedErrorPath: ['name'],
+ },
+ {
+ name: 'name containing an img onerror payload',
+ request: { name: '
', nodes: [], connections: {} },
+ expectedErrorPath: ['name'],
+ },
+ {
+ name: 'name containing inline HTML markup',
+ request: { name: 'Report bold', nodes: [], connections: {} },
+ expectedErrorPath: ['name'],
+ },
{
name: 'missing nodes',
request: { name: 'Test', connections: {} },
diff --git a/packages/@n8n/api-types/src/dto/workflows/__tests__/update-workflow.dto.test.ts b/packages/@n8n/api-types/src/dto/workflows/__tests__/update-workflow.dto.test.ts
index 50fcaf0c57b..cf3b20027b8 100644
--- a/packages/@n8n/api-types/src/dto/workflows/__tests__/update-workflow.dto.test.ts
+++ b/packages/@n8n/api-types/src/dto/workflows/__tests__/update-workflow.dto.test.ts
@@ -88,6 +88,21 @@ describe('UpdateWorkflowDto', () => {
request: { name: 'a'.repeat(129) },
expectedErrorPath: ['name'],
},
+ {
+ name: 'name containing a script tag',
+ request: { name: '' },
+ expectedErrorPath: ['name'],
+ },
+ {
+ name: 'name containing an img onerror payload',
+ request: { name: '
' },
+ expectedErrorPath: ['name'],
+ },
+ {
+ name: 'name containing inline HTML markup',
+ request: { name: 'Report bold' },
+ expectedErrorPath: ['name'],
+ },
{
name: 'invalid nodes type',
request: { nodes: 'not-an-array' },
diff --git a/packages/@n8n/api-types/src/dto/workflows/base-workflow.dto.ts b/packages/@n8n/api-types/src/dto/workflows/base-workflow.dto.ts
index 71145354258..9c32f1ff079 100644
--- a/packages/@n8n/api-types/src/dto/workflows/base-workflow.dto.ts
+++ b/packages/@n8n/api-types/src/dto/workflows/base-workflow.dto.ts
@@ -1,6 +1,8 @@
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) */
@@ -17,7 +19,8 @@ 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();
diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts
index 9c43bb4431f..882311e10d7 100644
--- a/packages/@n8n/api-types/src/index.ts
+++ b/packages/@n8n/api-types/src/index.ts
@@ -438,6 +438,7 @@ export {
} from './schemas/eval-collections.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 {
diff --git a/packages/@n8n/api-types/src/schemas/__tests__/data-table.schema.test.ts b/packages/@n8n/api-types/src/schemas/__tests__/data-table.schema.test.ts
new file mode 100644
index 00000000000..b08af46bf85
--- /dev/null
+++ b/packages/@n8n/api-types/src/schemas/__tests__/data-table.schema.test.ts
@@ -0,0 +1,36 @@
+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', ''],
+ ['contains an img onerror payload', '
'],
+ ['contains inline HTML markup', 'Customers bold'],
+ ['contains an svg onload payload', '