fix: harden startup telemetry urls

This commit is contained in:
lucaronin
2026-04-20 23:38:41 +02:00
parent ec8b637c84
commit 6a4046915c
2 changed files with 109 additions and 37 deletions

View File

@@ -5,6 +5,19 @@ import {
sanitizeTelemetryEnvValue,
} from './telemetryConfig'
function resolveConfig(overrides: {
VITE_SENTRY_DSN?: string
VITE_POSTHOG_KEY?: string
VITE_POSTHOG_HOST?: string
} = {}) {
return resolveFrontendTelemetryConfig({
VITE_SENTRY_DSN: 'https://public@example.ingest.sentry.io/123456',
VITE_POSTHOG_KEY: 'phc_test_key',
VITE_POSTHOG_HOST: 'https://eu.i.posthog.com',
...overrides,
})
}
describe('sanitizeTelemetryEnvValue', () => {
it('trims surrounding whitespace', () => {
expect(sanitizeTelemetryEnvValue(' value ')).toBe('value')
@@ -22,50 +35,77 @@ describe('sanitizeTelemetryEnvValue', () => {
})
describe('resolveFrontendTelemetryConfig', () => {
it('keeps valid telemetry values after sanitizing them', () => {
expect(resolveFrontendTelemetryConfig({
VITE_SENTRY_DSN: ' "https://public@example.ingest.sentry.io/123456" ',
VITE_POSTHOG_KEY: " 'phc_test_key' ",
VITE_POSTHOG_HOST: ' https://eu.i.posthog.com ',
})).toEqual({
sentryDsn: 'https://public@example.ingest.sentry.io/123456',
posthogKey: 'phc_test_key',
posthogHost: 'https://eu.i.posthog.com',
})
it.each([
{
name: 'keeps valid telemetry values after sanitizing them',
overrides: {
VITE_SENTRY_DSN: ' "https://public@example.ingest.sentry.io/123456" ',
VITE_POSTHOG_KEY: " 'phc_test_key' ",
VITE_POSTHOG_HOST: ' https://eu.i.posthog.com ',
},
expected: {
sentryDsn: 'https://public@example.ingest.sentry.io/123456',
posthogKey: 'phc_test_key',
posthogHost: 'https://eu.i.posthog.com',
},
},
{
name: 'adds https to scheme-less DSNs and PostHog hosts',
overrides: {
VITE_SENTRY_DSN: 'public@example.ingest.sentry.io/123456',
VITE_POSTHOG_KEY: 'phc_test_key',
VITE_POSTHOG_HOST: 'eu.i.posthog.com',
},
expected: {
sentryDsn: 'https://public@example.ingest.sentry.io/123456',
posthogKey: 'phc_test_key',
posthogHost: 'https://eu.i.posthog.com',
},
},
])('$name', ({ overrides, expected }) => {
expect(resolveConfig(overrides)).toEqual(expected)
})
it('uses the default PostHog host when one is not configured', () => {
expect(resolveFrontendTelemetryConfig({
VITE_SENTRY_DSN: 'https://public@example.ingest.sentry.io/123456',
VITE_POSTHOG_KEY: 'phc_test_key',
}).posthogHost).toBe(defaultPostHogHost)
})
it('adds https to scheme-less DSNs and PostHog hosts', () => {
expect(resolveFrontendTelemetryConfig({
VITE_SENTRY_DSN: 'public@example.ingest.sentry.io/123456',
VITE_POSTHOG_KEY: 'phc_test_key',
VITE_POSTHOG_HOST: 'eu.i.posthog.com',
})).toEqual({
sentryDsn: 'https://public@example.ingest.sentry.io/123456',
posthogKey: 'phc_test_key',
posthogHost: 'https://eu.i.posthog.com',
})
expect(resolveConfig({ VITE_POSTHOG_HOST: undefined }).posthogHost).toBe(defaultPostHogHost)
})
it('drops invalid Sentry DSNs instead of passing them to the SDK', () => {
expect(resolveFrontendTelemetryConfig({
VITE_SENTRY_DSN: 'not a dsn',
VITE_POSTHOG_KEY: 'phc_test_key',
VITE_POSTHOG_HOST: 'https://eu.i.posthog.com',
}).sentryDsn).toBe('')
expect(resolveConfig({ VITE_SENTRY_DSN: 'not a dsn' }).sentryDsn).toBe('')
})
it('drops invalid PostHog hosts instead of loading scripts from them', () => {
expect(resolveFrontendTelemetryConfig({
VITE_SENTRY_DSN: 'https://public@example.ingest.sentry.io/123456',
VITE_POSTHOG_KEY: 'phc_test_key',
VITE_POSTHOG_HOST: 'not a url',
}).posthogHost).toBeNull()
expect(resolveConfig({ VITE_POSTHOG_HOST: 'not a url' }).posthogHost).toBeNull()
})
it('drops placeholder telemetry hosts that would create broken startup requests', () => {
expect(resolveConfig({
VITE_SENTRY_DSN: 'https://public@false/123456',
VITE_POSTHOG_HOST: 'false',
})).toEqual({
sentryDsn: '',
posthogKey: 'phc_test_key',
posthogHost: null,
})
})
it('drops single-label telemetry hosts but keeps localhost for dev', () => {
expect(resolveConfig({
VITE_SENTRY_DSN: 'https://public@le/123456',
VITE_POSTHOG_HOST: 'https://le',
})).toEqual({
sentryDsn: '',
posthogKey: 'phc_test_key',
posthogHost: null,
})
expect(resolveConfig({
VITE_SENTRY_DSN: 'http://public@localhost:9000/123456',
VITE_POSTHOG_HOST: 'http://localhost:8010',
})).toEqual({
sentryDsn: 'http://public@localhost:9000/123456',
posthogKey: 'phc_test_key',
posthogHost: 'http://localhost:8010',
})
})
})

View File

@@ -1,4 +1,12 @@
const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com'
const DISALLOWED_TELEMETRY_HOSTS = new Set([
'false',
'true',
'null',
'undefined',
'none',
'disabled',
])
type TelemetryEnv = {
VITE_SENTRY_DSN?: string
@@ -35,12 +43,36 @@ export function sanitizeTelemetryEnvValue(value: string | undefined): string {
function isHttpUrl(value: string): boolean {
try {
const url = new URL(value)
return url.protocol === 'http:' || url.protocol === 'https:'
return (url.protocol === 'http:' || url.protocol === 'https:')
&& isAllowedTelemetryHostname(url.hostname)
} catch {
return false
}
}
function normalizeHostname(hostname: string): string {
const normalized = hostname.trim().replace(/\.$/, '').toLowerCase()
if (normalized.startsWith('[') && normalized.endsWith(']')) {
return normalized.slice(1, -1)
}
return normalized
}
function isIpAddress(hostname: string): boolean {
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
return hostname.split('.').every((segment) => Number(segment) <= 255)
}
return hostname.includes(':') && /^[\da-f:]+$/i.test(hostname)
}
function isAllowedTelemetryHostname(hostname: string): boolean {
const normalized = normalizeHostname(hostname)
if (!normalized || DISALLOWED_TELEMETRY_HOSTS.has(normalized)) return false
if (normalized === 'localhost') return true
return normalized.includes('.') || isIpAddress(normalized)
}
function normalizeHttpLikeValue(value: string): string {
if (!value) return ''
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value)) return value