1- import { Injectable , type NestMiddleware } from '@nestjs/common' ;
1+ import { Injectable , Logger , type NestMiddleware } from '@nestjs/common' ;
22import { InjectRepository } from '@nestjs/typeorm' ;
33
4+ import { createHash , timingSafeEqual } from 'crypto' ;
45import { type NextFunction , type Request , type Response } from 'express' ;
56import { Repository } from 'typeorm' ;
67
78import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity' ;
89
10+ /** SHA-256 hash a string and return a Buffer for timingSafeEqual. */
11+ const sha256 = ( value : string ) : Buffer =>
12+ createHash ( 'sha256' ) . update ( value ) . digest ( ) ;
13+
14+ /** Simple in-memory sliding-window rate limiter (per IP). */
15+ class AdminTokenRateLimiter {
16+ private readonly attempts = new Map < string , number [ ] > ( ) ;
17+ private readonly maxAttempts : number ;
18+ private readonly windowMs : number ;
19+
20+ constructor ( maxAttempts = 10 , windowMs = 60_000 ) {
21+ this . maxAttempts = maxAttempts ;
22+ this . windowMs = windowMs ;
23+ }
24+
25+ isRateLimited ( ip : string ) : boolean {
26+ const now = Date . now ( ) ;
27+ const timestamps = this . attempts . get ( ip ) ?? [ ] ;
28+ const recent = timestamps . filter ( ( t ) => now - t < this . windowMs ) ;
29+
30+ this . attempts . set ( ip , recent ) ;
31+
32+ return recent . length >= this . maxAttempts ;
33+ }
34+
35+ record ( ip : string ) : void {
36+ const now = Date . now ( ) ;
37+ const timestamps = this . attempts . get ( ip ) ?? [ ] ;
38+
39+ timestamps . push ( now ) ;
40+ this . attempts . set ( ip , timestamps ) ;
41+ }
42+ }
43+
944@Injectable ( )
1045export class AdminTokenMiddleware implements NestMiddleware {
11- private readonly adminToken : string | undefined ;
46+ private readonly logger = new Logger ( AdminTokenMiddleware . name ) ;
47+ private readonly adminTokenHash : Buffer | undefined ;
48+ private readonly rateLimiter = new AdminTokenRateLimiter ( 10 , 60_000 ) ;
1249
1350 constructor (
1451 @InjectRepository ( WorkspaceEntity )
1552 private readonly workspaceRepository : Repository < WorkspaceEntity > ,
1653 ) {
17- this . adminToken = process . env . EXE_CRM_ADMIN_TOKEN ;
54+ const raw = process . env . EXE_CRM_ADMIN_TOKEN ;
55+
56+ if ( raw ) {
57+ this . adminTokenHash = sha256 ( raw ) ;
58+ }
1859 }
1960
2061 async use ( req : Request , _res : Response , next : NextFunction ) {
21- if ( ! this . adminToken ) {
62+ if ( ! this . adminTokenHash ) {
2263 next ( ) ;
2364
2465 return ;
@@ -33,8 +74,35 @@ export class AdminTokenMiddleware implements NestMiddleware {
3374 }
3475
3576 const token = authHeader . slice ( 7 ) ;
77+ const clientIp =
78+ ( req . headers [ 'x-forwarded-for' ] as string ) ?. split ( ',' ) [ 0 ] ?. trim ( ) ??
79+ req . socket . remoteAddress ??
80+ 'unknown' ;
81+
82+ // Rate-limit check before any comparison
83+ if ( this . rateLimiter . isRateLimited ( clientIp ) ) {
84+ this . logger . warn (
85+ `Admin token rate limit exceeded for IP=${ clientIp } ` ,
86+ ) ;
87+
88+ next ( ) ;
89+
90+ return ;
91+ }
92+
93+ this . rateLimiter . record ( clientIp ) ;
94+
95+ // Timing-safe comparison using SHA-256 hashes
96+ const incomingHash = sha256 ( token ) ;
97+
98+ if (
99+ incomingHash . length !== this . adminTokenHash . length ||
100+ ! timingSafeEqual ( incomingHash , this . adminTokenHash )
101+ ) {
102+ this . logger . warn (
103+ `Admin token rejected — IP=${ clientIp } path=${ req . path } ` ,
104+ ) ;
36105
37- if ( token !== this . adminToken ) {
38106 next ( ) ;
39107
40108 return ;
@@ -55,6 +123,10 @@ export class AdminTokenMiddleware implements NestMiddleware {
55123 req . workspaceId = workspace . id ;
56124 req . adminTokenAuthenticated = true ;
57125
126+ this . logger . log (
127+ `Admin token accepted — IP=${ clientIp } workspace=${ workspace . id } path=${ req . path } ` ,
128+ ) ;
129+
58130 next ( ) ;
59131 }
60132}
0 commit comments