diff --git a/server/src/utils/ssrfGuard.ts b/server/src/utils/ssrfGuard.ts index 3f48b4001..ea0a1011c 100644 --- a/server/src/utils/ssrfGuard.ts +++ b/server/src/utils/ssrfGuard.ts @@ -541,7 +541,7 @@ export function proxyFor(rawUrl: string, proxy = PROXY): string | null { const port = url.port || (url.protocol === 'https:' ? '443' : '80'); const bypass = proxy.noProxy.some((entry) => { if (entry === '*') return true; - const [name, entryPort] = entry.replace(/^\[|\](?=:|$)/g, '').split(/:(?=\d+$)/); + const [name, entryPort] = entry.replace(/(?:^\[)|(?:\](?=:|$))/g, '').split(/:(?=\d+$)/); if (entryPort && entryPort !== port) return false; const bare = name.replace(/^\*?\./, ''); return host === bare || host.endsWith(`.${bare}`); diff --git a/shared/src/place/place-hours.spec.ts b/shared/src/place/place-hours.spec.ts index 15e65d778..af7cadb35 100644 --- a/shared/src/place/place-hours.spec.ts +++ b/shared/src/place/place-hours.spec.ts @@ -29,6 +29,10 @@ describe('place hours and e-mail (#2472)', () => { expect(placeEmailField.safeParse('a@b.co').success).toBe(true); expect(placeEmailField.safeParse('').success).toBe(true); expect(placeEmailField.safeParse('nope').success).toBe(false); + for (const bad of ['@b.co', 'a@@b.co', 'a@b@c.co', 'a@bco', 'a@.co', 'a@b.', 'a b@c.co']) { + expect(placeEmailField.safeParse(bad).success).toBe(false); + } + expect(placeEmailField.safeParse(' a.b@mail.example.org ').success).toBe(true); expect(placeCreateRequestSchema.safeParse({ name: 'X', email: 'nope' }).success).toBe(false); expect(placeUpdateRequestSchema.safeParse({ opening_hours: WEEK, email: null }).success).toBe(true); }); diff --git a/shared/src/place/place-hours.ts b/shared/src/place/place-hours.ts index 6d53ba3f2..196f397da 100644 --- a/shared/src/place/place-hours.ts +++ b/shared/src/place/place-hours.ts @@ -47,12 +47,19 @@ export const placeOpeningHoursField = z .nullable() .optional(); -const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +/** One @ with something on both sides, a dot inside the domain, no whitespace anywhere. */ +function isEmail(value: string): boolean { + if (/\s/.test(value)) return false; + const at = value.indexOf('@'); + if (at < 1 || at !== value.lastIndexOf('@')) return false; + // A dot with something on both sides, like the former /[^\s@]+\.[^\s@]+$/ domain part. + return value.slice(at + 2, -1).includes('.'); +} /** An e-mail address, or empty to clear it. */ export const placeEmailField = z .string() .max(254) - .refine((v) => v === '' || EMAIL.test(v.trim()), { message: 'email must be an e-mail address' }) + .refine((v) => v === '' || isEmail(v.trim()), { message: 'email must be an e-mail address' }) .nullable() .optional();