mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(api): resolve client ip from peer when untrusted (#3420)
This commit is contained in:
1 parent
17746fcdc4
commit
68cb2a4bbb
13 files changed
+208
-54
No files matched your search
@@ -9,7 +9,9 @@ import {
|
||||
requireClientIp,
|
||||
resolveClientIpHeaderName,
|
||||
} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import type {HttpBindings} from '@hono/node-server';
|
||||
import type {Context} from 'hono';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
|
||||
export interface ClientIpResolution {
|
||||
trustClientIpHeader: boolean;
|
||||
@@ -22,6 +24,20 @@ interface ClientIpResolutionOptions {
|
||||
clientIpHeaderName: string;
|
||||
}
|
||||
|
||||
const peerAddresses = new WeakMap<Request, string>();
|
||||
|
||||
export const PeerAddressMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||
const remoteAddress = (ctx.env as Partial<HttpBindings> | undefined)?.incoming?.socket?.remoteAddress;
|
||||
if (remoteAddress) {
|
||||
peerAddresses.set(ctx.req.raw, remoteAddress);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
function withPeerAddress(request: Request, options: ClientIpResolutionOptions) {
|
||||
return {...options, peerAddress: peerAddresses.get(request) ?? null};
|
||||
}
|
||||
|
||||
export function resolveClientIpWithOptions(ctx: Context<HonoEnv>, options: ClientIpResolutionOptions): string | null {
|
||||
const clientIpHeaderName = resolveClientIpHeaderName(options.clientIpHeaderName);
|
||||
const {trustClientIpHeader} = options;
|
||||
@@ -33,7 +49,7 @@ export function resolveClientIpWithOptions(ctx: Context<HonoEnv>, options: Clien
|
||||
) {
|
||||
return cached.ip;
|
||||
}
|
||||
const ip = extractClientIp(ctx.req.raw, {trustClientIpHeader, clientIpHeaderName});
|
||||
const ip = extractClientIp(ctx.req.raw, withPeerAddress(ctx.req.raw, {trustClientIpHeader, clientIpHeaderName}));
|
||||
ctx.set('clientIpResolution', {trustClientIpHeader, clientIpHeaderName, ip});
|
||||
return ip;
|
||||
}
|
||||
@@ -43,11 +59,11 @@ function proxyClientIpOptions(proxy: APIConfig['proxy']): ClientIpResolutionOpti
|
||||
}
|
||||
|
||||
export function extractConfiguredClientIp(request: Request): string | null {
|
||||
return extractClientIp(request, proxyClientIpOptions(Config.proxy));
|
||||
return extractClientIp(request, withPeerAddress(request, proxyClientIpOptions(Config.proxy)));
|
||||
}
|
||||
|
||||
export function requireConfiguredClientIp(request: Request, proxy: APIConfig['proxy']): string {
|
||||
return requireClientIp(request, proxyClientIpOptions(proxy));
|
||||
return requireClientIp(request, withPeerAddress(request, proxyClientIpOptions(proxy)));
|
||||
}
|
||||
|
||||
export function getRequestClientIp(ctx: Context<HonoEnv>): string | null {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {HonoEnv} from '@app/api/types/HonoEnv';
|
||||
import {getRequestClientIp, requireRequestClientIp, resolveClientIpWithOptions} from '@app/api/utils/RequestClientIp';
|
||||
import {
|
||||
extractConfiguredClientIp,
|
||||
getRequestClientIp,
|
||||
PeerAddressMiddleware,
|
||||
requireConfiguredClientIp,
|
||||
requireRequestClientIp,
|
||||
resolveClientIpWithOptions,
|
||||
} from '@app/api/utils/RequestClientIp';
|
||||
import {AppErrorHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
|
||||
import {MissingClientIpError} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import {Hono} from 'hono';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {afterEach, describe, expect, it} from 'vitest';
|
||||
|
||||
interface CountingRequest {
|
||||
request: Request;
|
||||
@@ -28,6 +36,12 @@ function createCountingRequest(headers: Record<string, string>): CountingRequest
|
||||
}
|
||||
|
||||
describe('RequestClientIp', () => {
|
||||
const previousTrustClientIpHeader = Config.proxy.trust_client_ip_header;
|
||||
|
||||
afterEach(() => {
|
||||
Config.proxy.trust_client_ip_header = previousTrustClientIpHeader;
|
||||
});
|
||||
|
||||
it('normalizes the resolved address the same way for every consumer', async () => {
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
@@ -102,4 +116,22 @@ describe('RequestClientIp', () => {
|
||||
expect(response.status).toBe(403);
|
||||
expect(await response.json()).toMatchObject({code: 'FORBIDDEN'});
|
||||
});
|
||||
it('resolves the peer address for request only callers when the header is untrusted', async () => {
|
||||
Config.proxy.trust_client_ip_header = false;
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.use(PeerAddressMiddleware);
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
seen.push(extractConfiguredClientIp(ctx.req.raw));
|
||||
seen.push(requireConfiguredClientIp(ctx.req.raw, Config.proxy));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
await app.request(
|
||||
'http://localhost/v1/messages',
|
||||
{headers: {'x-forwarded-for': '203.0.113.10'}},
|
||||
{incoming: {socket: {remoteAddress: '::ffff:198.51.100.20'}}},
|
||||
);
|
||||
expect(seen).toEqual(['198.51.100.20', '198.51.100.20', '198.51.100.20']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user