Skip to content

Commit 2258734

Browse files
authored
fix: precompile x-forwarded-host regexp (#1203)
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent 6f5a298 commit 2258734

6 files changed

Lines changed: 150 additions & 22 deletions

File tree

src/http/plugins/tenant-id.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getXForwardedHostRegExp } from '@internal/http/x-forwarded-host'
12
import fastifyPlugin from 'fastify-plugin'
23
import { getConfig } from '../../config'
34

@@ -7,13 +8,15 @@ declare module 'fastify' {
78
}
89
}
910

10-
const { isMultitenant, tenantId: defaultTenantId, requestXForwardedHostRegExp } = getConfig()
11+
const { isMultitenant, tenantId: defaultTenantId } = getConfig()
12+
13+
const xForwardedHostRegExp = getXForwardedHostRegExp()
1114

1215
export const tenantId = fastifyPlugin(
1316
async (fastify) => {
1417
fastify.decorateRequest('tenantId', defaultTenantId)
1518
fastify.addHook('onRequest', (request, _reply, done) => {
16-
if (!isMultitenant || !requestXForwardedHostRegExp) {
19+
if (!isMultitenant || !xForwardedHostRegExp) {
1720
done()
1821
return
1922
}
@@ -24,7 +27,7 @@ export const tenantId = fastifyPlugin(
2427
return
2528
}
2629

27-
const result = xForwardedHost.match(requestXForwardedHostRegExp)
30+
const result = xForwardedHost.match(xForwardedHostRegExp)
2831
if (!result) {
2932
done()
3033
return

src/internal/database/client.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { Cluster } from '@internal/cluster'
22
import { ERRORS } from '@internal/errors'
3+
import { getXForwardedHostRegExp } from '@internal/http/x-forwarded-host'
34
import { getConfig } from '../../config'
45
import { PgTenantConnection } from './pg-connection'
56
import { User } from './pool'
67
import { getTenantConfig } from './tenant'
78

9+
const xForwardedHostRegExp = getXForwardedHostRegExp()
10+
811
interface ConnectionOptions {
912
host: string
1013
tenantId: string
@@ -38,13 +41,7 @@ async function getDbSettings(
3841
host: string | undefined,
3942
options?: { disableHostCheck?: boolean }
4043
) {
41-
const {
42-
isMultitenant,
43-
databasePoolURL,
44-
databaseURL,
45-
databaseMaxConnections,
46-
requestXForwardedHostRegExp,
47-
} = getConfig()
44+
const { isMultitenant, databasePoolURL, databaseURL, databaseMaxConnections } = getConfig()
4845

4946
let dbUrl = databasePoolURL || databaseURL
5047
let maxConnections = databaseMaxConnections
@@ -55,16 +52,18 @@ async function getDbSettings(
5552
throw ERRORS.InvalidTenantId()
5653
}
5754

58-
if (requestXForwardedHostRegExp && !options?.disableHostCheck) {
59-
const xForwardedHost = host
55+
if (!options?.disableHostCheck) {
56+
if (xForwardedHostRegExp) {
57+
const xForwardedHost = host
6058

61-
if (typeof xForwardedHost !== 'string') {
62-
throw ERRORS.InvalidXForwardedHeader('X-Forwarded-Host header is not a string')
63-
}
64-
if (!new RegExp(requestXForwardedHostRegExp).test(xForwardedHost)) {
65-
throw ERRORS.InvalidXForwardedHeader(
66-
'X-Forwarded-Host header does not match regular expression'
67-
)
59+
if (typeof xForwardedHost !== 'string') {
60+
throw ERRORS.InvalidXForwardedHeader('X-Forwarded-Host header is not a string')
61+
}
62+
if (!xForwardedHostRegExp.test(xForwardedHost)) {
63+
throw ERRORS.InvalidXForwardedHeader(
64+
'X-Forwarded-Host header does not match regular expression'
65+
)
66+
}
6867
}
6968
}
7069

src/internal/http/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './agent'
22
export * from './header'
3+
export * from './x-forwarded-host'
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const xForwardedHostEnvKeys = [
2+
'MULTI_TENANT',
3+
'IS_MULTITENANT',
4+
'REQUEST_X_FORWARDED_HOST_REGEXP',
5+
'X_FORWARDED_HOST_REGEXP',
6+
] as const
7+
8+
const originalEnv = Object.fromEntries(
9+
xForwardedHostEnvKeys.map((key) => [key, process.env[key]])
10+
) as Record<(typeof xForwardedHostEnvKeys)[number], string | undefined>
11+
12+
async function loadXForwardedHostRegExp({
13+
isMultitenant,
14+
pattern,
15+
}: {
16+
isMultitenant: boolean
17+
pattern?: string
18+
}) {
19+
vi.resetModules()
20+
21+
process.env.MULTI_TENANT = isMultitenant ? 'true' : 'false'
22+
process.env.IS_MULTITENANT = isMultitenant ? 'true' : 'false'
23+
process.env.X_FORWARDED_HOST_REGEXP = ''
24+
25+
if (pattern === undefined) {
26+
process.env.REQUEST_X_FORWARDED_HOST_REGEXP = ''
27+
} else {
28+
process.env.REQUEST_X_FORWARDED_HOST_REGEXP = pattern
29+
}
30+
31+
return await import('./x-forwarded-host')
32+
}
33+
34+
function restoreXForwardedHostEnv() {
35+
for (const key of xForwardedHostEnvKeys) {
36+
const value = originalEnv[key]
37+
if (value === undefined) {
38+
delete process.env[key]
39+
} else {
40+
process.env[key] = value
41+
}
42+
}
43+
}
44+
45+
afterEach(() => {
46+
restoreXForwardedHostEnv()
47+
vi.resetModules()
48+
})
49+
50+
describe('getXForwardedHostRegExp', () => {
51+
it('skips compiling the host pattern when multitenancy is disabled', async () => {
52+
const { getXForwardedHostRegExp } = await loadXForwardedHostRegExp({
53+
isMultitenant: false,
54+
pattern: '[',
55+
})
56+
57+
expect(getXForwardedHostRegExp()).toBeUndefined()
58+
})
59+
60+
it('returns undefined when no host pattern is configured', async () => {
61+
const { getXForwardedHostRegExp } = await loadXForwardedHostRegExp({
62+
isMultitenant: true,
63+
})
64+
65+
expect(getXForwardedHostRegExp()).toBeUndefined()
66+
})
67+
68+
it('reuses the compiled regexp from startup config', async () => {
69+
const { getXForwardedHostRegExp } = await loadXForwardedHostRegExp({
70+
isMultitenant: true,
71+
pattern: '^([a-z]+)\\.local$',
72+
})
73+
74+
const first = getXForwardedHostRegExp()
75+
const second = getXForwardedHostRegExp()
76+
77+
expect(second).toBe(first)
78+
expect('tenant.local'.match(first!)).toBeTruthy()
79+
})
80+
81+
it('does not recompile when config is reloaded after module load', async () => {
82+
const { getXForwardedHostRegExp } = await loadXForwardedHostRegExp({
83+
isMultitenant: true,
84+
pattern: '^([a-z]+)\\.local$',
85+
})
86+
const previous = getXForwardedHostRegExp()
87+
88+
process.env.REQUEST_X_FORWARDED_HOST_REGEXP = '^([0-9]+)\\.local$'
89+
const { getConfig } = await import('../../config')
90+
getConfig({ reload: true })
91+
92+
const current = getXForwardedHostRegExp()
93+
94+
expect(current).toBe(previous)
95+
expect('tenant.local'.match(current!)).toBeTruthy()
96+
expect('123.local'.match(current!)).toBeFalsy()
97+
})
98+
99+
it('throws while loading the helper when the configured pattern is invalid', async () => {
100+
await expect(
101+
loadXForwardedHostRegExp({
102+
isMultitenant: true,
103+
pattern: '[',
104+
})
105+
).rejects.toThrow(SyntaxError)
106+
})
107+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { getConfig } from '../../config'
2+
3+
const xForwardedHostRegExp = createXForwardedHostRegExp()
4+
5+
function createXForwardedHostRegExp(): RegExp | undefined {
6+
const { isMultitenant, requestXForwardedHostRegExp } = getConfig()
7+
if (!isMultitenant || !requestXForwardedHostRegExp) {
8+
return undefined
9+
}
10+
11+
return new RegExp(requestXForwardedHostRegExp)
12+
}
13+
14+
export function getXForwardedHostRegExp(): RegExp | undefined {
15+
return xForwardedHostRegExp
16+
}

src/internal/monitoring/otel-tracing.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import { getXForwardedHostRegExp } from '@internal/http/x-forwarded-host'
12
import { getConfig } from '../../config'
23

34
const {
45
version,
56
requestTraceHeader,
67
isMultitenant,
7-
requestXForwardedHostRegExp,
88
tenantId: defaultTenantId,
99
region,
1010
serviceName,
1111
storageS3InternalTracesEnabled,
1212
} = getConfig()
1313

14+
const xForwardedHostRegExp = getXForwardedHostRegExp()
15+
1416
import { FastifyOtelInstrumentation } from '@fastify/otel'
1517
import * as grpc from '@grpc/grpc-js'
1618
import { logger, logSchema } from '@internal/monitoring/logger'
@@ -154,11 +156,11 @@ if (tracingEnabled && traceExporter && spanProcessors.length > 0) {
154156
startIncomingSpanHook: (req) => {
155157
let tenantId = ''
156158
if (isMultitenant) {
157-
if (requestXForwardedHostRegExp) {
159+
if (xForwardedHostRegExp) {
158160
const serverRequest = req
159161
const xForwardedHost = serverRequest.headers['x-forwarded-host']
160162
if (typeof xForwardedHost !== 'string') return {}
161-
const result = xForwardedHost.match(requestXForwardedHostRegExp)
163+
const result = xForwardedHost.match(xForwardedHostRegExp)
162164
if (!result) return {}
163165
tenantId = result[1]
164166
}

0 commit comments

Comments
 (0)