fix: harden startup telemetry urls
This commit is contained in:
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user