Skip to content

Commit 1036476

Browse files
committed
fix: fallback to the tcp connection adress if the ip is not provided in headers
1 parent f176717 commit 1036476

4 files changed

Lines changed: 14 additions & 5 deletions

File tree

‎adminforth/auth.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AdminUserAuthorizationResult, AdminUserAuthorizeFunction, AfterSessionC
66
import { AdminUser } from './types/Common.js';
77
import { listify, removedInV4 } from './modules/utils.js';
88
import { afLogger } from './modules/logger.js';
9+
import { getRequestContext } from './modules/requestContext.js';
910
import { Address4, Address6 } from 'ip-address';
1011

1112
// Returns true for valid IPs which are not globally reachable (private, loopback, link-local, CGNAT, reserved, etc.)
@@ -19,6 +20,11 @@ function isIpPrivate(ip: string | null): boolean {
1920
return false;
2021
}
2122

23+
// Node reports IPv4 clients of a dual-stack server as IPv4-mapped IPv6 addresses, e.g. ::ffff:1.2.3.4
24+
function unmapIpv4(ip: string | null): string | null {
25+
return Address6.isValid(ip) && new Address6(ip).is4() ? new Address6(ip).to4().correctForm() : ip;
26+
}
27+
2228
// Function to generate a password hash using PBKDF2
2329
function calcPasswordHash(password, salt, iterations = 100000, keyLength = 64, digest = 'sha512') {
2430
return new Promise((resolve, reject) => {
@@ -85,6 +91,10 @@ class AdminForthAuth implements IAdminForthAuth {
8591
headersLower['x-host'] ||
8692
null;
8793
}
94+
if (!ip || isIpPrivate(ip)) {
95+
// no proxy header or an internal address in it, so the client is connected directly
96+
ip = unmapIpv4(getRequestContext()?.remoteAddress ?? null);
97+
}
8898
if (isIpPrivate(ip)) {
8999
return null;
90100
}

‎adminforth/modules/requestContext.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const ADMINFORTH_CLIENT_ID_HEADER = 'x-adminforth-client-id';
44

55
export type AdminForthRequestContext = {
66
websocketClientId?: string;
7+
remoteAddress?: string;
78
};
89

910
const requestContextStorage = new AsyncLocalStorage<AdminForthRequestContext>();

‎adminforth/servers/express.ts‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,7 @@ class ExpressServer implements IExpressHttpServer {
300300

301301
serve(app) {
302302
this.expressApp = app;
303-
this.expressApp.use((req, res, next) => {
304-
runWithRequestContext({
305-
websocketClientId: getHeaderString(req.headers, ADMINFORTH_CLIENT_ID_HEADER),
306-
}, next);
307-
});
303+
this.expressApp.use((req, res, next) => this.runInRequestContext(req, next));
308304
this.patchSchemaAwareRouteRegistration();
309305
this.flushPendingEndpointRegistrations();
310306
// Express 5 exposes the router as `app.router`; Express 4 used the internal `app._router`.
@@ -342,6 +338,7 @@ class ExpressServer implements IExpressHttpServer {
342338
runInRequestContext(req, callback) {
343339
return runWithRequestContext({
344340
websocketClientId: getHeaderString(req.headers, ADMINFORTH_CLIENT_ID_HEADER),
341+
remoteAddress: req.socket.remoteAddress,
345342
}, callback);
346343
}
347344

‎documentation/docs/tutorial/03-Customization/12-security.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ This function is used for:
166166
- Can be used by your own code e.g. hooks to write first login/last login IP address to db
167167
168168
By default it takes the last IP address from `X-Forwarded-For` header. Proxies append the address of their client to the end of this header and keep everything the client sent before it, so the last address is the one added by your nearest proxy.
169+
If there is no such header or it has a private address, AdminForth uses the address of the TCP connection, which is the client itself when there is no proxy in front of the app.
169170
AdminForth does not understand whether this header can be trusted or no. If there is no proxy in front of the app, the whole header comes from the client and might be spoofed like this:
170171
171172
```

0 commit comments

Comments
 (0)