Skip to content

Commit b4c864d

Browse files
extract shared email header safety regex
Co-authored-by: Theodore Li <TheodoreSpeaks@users.noreply.github.com>
1 parent 730097d commit b4c864d

File tree

6 files changed

+38
-9
lines changed

6 files changed

+38
-9
lines changed

apps/sim/app/api/help/integration-request/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
66
import { RateLimiter } from '@/lib/core/rate-limiter'
77
import { generateRequestId } from '@/lib/core/utils/request'
88
import { getEmailDomain } from '@/lib/core/utils/urls'
9+
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/header-safety'
910
import { sendEmail } from '@/lib/messaging/email/mailer'
1011
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
1112

1213
const logger = createLogger('IntegrationRequestAPI')
13-
const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n]*$/
1414

1515
const rateLimiter = new RateLimiter()
1616

apps/sim/app/api/help/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { getSession } from '@/lib/auth'
66
import { env } from '@/lib/core/config/env'
77
import { generateRequestId } from '@/lib/core/utils/request'
88
import { getEmailDomain } from '@/lib/core/utils/urls'
9+
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/header-safety'
910
import { sendEmail } from '@/lib/messaging/email/mailer'
1011
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
1112

1213
const logger = createLogger('HelpAPI')
13-
const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n]*$/
1414

1515
const helpFormSchema = z.object({
1616
subject: z

apps/sim/lib/marketing/demo-request.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { z } from 'zod'
22
import { quickValidateEmail } from '@/lib/messaging/email/validation'
3-
4-
const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n]*$/
3+
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/header-safety'
54

65
export const DEMO_REQUEST_REGION_VALUES = [
76
'north_america',
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
EMAIL_HEADER_CONTROL_CHARS_REGEX,
7+
hasEmailHeaderControlChars,
8+
NO_EMAIL_HEADER_CONTROL_CHARS_REGEX,
9+
} from '@/lib/messaging/email/header-safety'
10+
11+
describe('email header safety', () => {
12+
it('rejects CRLF characters consistently', () => {
13+
const injectedHeader = 'Hello\r\nBcc: attacker@example.com'
14+
15+
expect(EMAIL_HEADER_CONTROL_CHARS_REGEX.test(injectedHeader)).toBe(true)
16+
expect(hasEmailHeaderControlChars(injectedHeader)).toBe(true)
17+
expect(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX.test(injectedHeader)).toBe(false)
18+
})
19+
20+
it('allows plain header content', () => {
21+
const safeHeader = 'Product feedback'
22+
23+
expect(EMAIL_HEADER_CONTROL_CHARS_REGEX.test(safeHeader)).toBe(false)
24+
expect(hasEmailHeaderControlChars(safeHeader)).toBe(false)
25+
expect(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX.test(safeHeader)).toBe(true)
26+
})
27+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const EMAIL_HEADER_CONTROL_CHARS_REGEX = /[\r\n]/
2+
3+
export const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n]*$/
4+
5+
export function hasEmailHeaderControlChars(value: string): boolean {
6+
return EMAIL_HEADER_CONTROL_CHARS_REGEX.test(value)
7+
}

apps/sim/lib/messaging/email/mailer.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { createLogger } from '@sim/logger'
33
import { Resend } from 'resend'
44
import { env } from '@/lib/core/config/env'
55
import { getBaseUrl } from '@/lib/core/utils/urls'
6+
import { hasEmailHeaderControlChars } from '@/lib/messaging/email/header-safety'
67
import { generateUnsubscribeToken, isUnsubscribed } from '@/lib/messaging/email/unsubscribe'
78
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
89

910
const logger = createLogger('Mailer')
10-
const EMAIL_HEADER_CONTROL_CHARS_REGEX = /[\r\n]/
1111

1212
export type EmailType = 'transactional' | 'marketing' | 'updates' | 'notifications'
1313

@@ -65,10 +65,6 @@ interface PreparedEmailHeaderData {
6565
replyTo?: string
6666
}
6767

68-
function hasEmailHeaderControlChars(value: string): boolean {
69-
return EMAIL_HEADER_CONTROL_CHARS_REGEX.test(value)
70-
}
71-
7268
function sanitizeEmailSubject(subject: string): string {
7369
return subject.replace(/[\r\n]+/g, ' ').trim()
7470
}

0 commit comments

Comments
 (0)