NestJS Security — Guards بتتكسر إزاي
Guards bypass و ValidationPipe و DI poisoning و GraphQL في Nest
ليه NestJS له فئة مخاطر خاصة؟
NestJS framework حلو. شغل enterprise، DI، decorators، كل حاجة "magic".
- طب ما هو ده اللي بيدّيله أمان أصلاً، صح؟
متوقّع كالعادة يا مستجد. الـ magic ده بيخبّي إيه؟ بيخبّي إن في 6 طبقات بين الـ request والـ logic بتاعتك. كل طبقة فيهم لو اتكتبت غلط = ثغرة.
{"isAdmin": true}. الـ ORM (TypeORM) خد الـ payload كله وحفظه. خلاص. الـ user بقى admin. مفيش 0day، مفيش APT — بس setting واحد ناقص في main.ts. ده اسمه Mass Assignment، ومش مشكلة Nest — مشكلة إن الناس بتثق في الـ "magic".بنية الطلب — أين يمكن أن يفشل الفحص
Request
↓
[Middleware] ← Express/Fastify level (helmet, body-parser)
↓
[Guards] ← AuthGuard, RolesGuard — hasOwnProperty: استثناء = bypass
↓
[Interceptors (before)] ← logging, transformation
↓
[Pipes] ← ValidationPipe, ParseIntPipe — هنا تُفحص الأنواع
↓
[Controller method] ← business logic
↓
[Interceptors (after)] ← serialization, response shaping
↓
Responseكل خطوة من دول نقطة فشل محتملة. Guard رمى exception غير متوقع؟ الـ filter ممكن يحوّله 500 ويسرّب stack trace. Pipe مش متربّط؟ الـ DTO هيعدّي من غير فحص أصلاً.
ValidationPipe — الفخ الأشهر
NestJS بيستخدم class-validator. تنسى whitelist: true أو forbidNonWhitelisted؟ المهاجم بيحقن خصايص زيادة في الـ DTO وأنت بتحفظهم في DB من غير ما تاخد بالك.
// خطر — DTO فيه isAdmin محذوف من الواجهة لكن موجود في DB
class CreateUserDto {
@IsString() name: string;
@IsEmail() email: string;
// isAdmin غير معرّف هنا...
}
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
return this.users.save(dto); // ينسخ كل البيانات، بما فيها isAdmin: true
}
}
// PoC
POST /users
{ "name": "x", "email": "x@x", "isAdmin": true } // ⇒ admin
// الإصلاح في main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // أزل الخصائص غير المعرّفة
forbidNonWhitelisted: true, // ارفض الطلب لو فيه خصائص إضافية
transform: true, // type-coerce
transformOptions: { enableImplicitConversion: false }, // لا coerce ضمنية
forbidUnknownValues: true
}));Mass assignment & DTO leakage
// طلب الـ entity كاملة في response؟ خطأ.
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.users.findOne(id); // يتضمن passwordHash, mfaSecret...
}
// الإصلاح — class-transformer + ClassSerializerInterceptor
class User {
id: number;
email: string;
@Exclude()
passwordHash: string;
@Exclude()
mfaSecret: string;
}
// في app.module
@UseInterceptors(ClassSerializerInterceptor)
@Controller('users')
export class UsersController { ... }
// أو DTOs منفصلة للـ response (الأفضل)
class UserResponseDto {
@Expose() id: number;
@Expose() email: string;
}
return plainToInstance(UserResponseDto, user, { excludeExtraneousValues: true });Guards bypass — أنماط شائعة
// Guard معتمد على property بسيطة
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest();
return req.user?.isAdmin === true;
}
}
// مع prototype pollution سابق، req.user يرث isAdmin: true من Object.prototype
// → كل user يصبح admin
// أيضاً — Guard يقرأ من req.headers بثقة
return req.headers['x-user-id'] === req.params.id;
// المهاجم يضيف header (مع trust proxy)- افحص بـ hasOwnProperty (أو Object.hasOwn) — مش inherited.
- متثقش في req.user إلا لو AuthGuard موثوق هو اللي حطّه.
- متبنيش حاجة على headers سهلة التزوير.
- RolesGuard: استخدم Reflector وأنت بتقرا الـ decorator، مش strings hard-coded.
- جرّب الـ Guard بنفسك على {__proto__: {isAdmin: true}} في الـ request — لو عدّى، عندك مشكلة.
JWT في NestJS — فخاخ
// كثير من tutorials يكتب
JwtModule.register({ secret: 'secret' });
// خطأ — نفس الـ secret في dev و prod
// خطأ — لا تحدد algorithms (يقبل HS256 و none و RS256 → key confusion)
// الصحيح
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.JWT_SECRET, // قوي + per-env
signOptions: { algorithm: 'HS256', expiresIn: '15m' },
verifyOptions: { algorithms: ['HS256'] } // قائمة صارمة
}),
});
// أو RS256 مع public key
JwtModule.register({
publicKey: fs.readFileSync('public.pem'),
privateKey: fs.readFileSync('private.pem'),
signOptions: { algorithm: 'RS256', expiresIn: '15m' },
verifyOptions: { algorithms: ['RS256'] }
});DI Container Poisoning
كل provider في الـ Module هو فعلياً singleton. لو حد قدر يعدّل provider في الـ runtime (نادر، بس ممكن عن طريق debugger أو unsafe eval)، هيأثر على كل الـ requests. الأشهر:
- Custom providers بـ useValue جايّة من user input — متعملش كده أبداً.
- Dynamic modules بتقرا config من ملف — file injection بيبقى RCE على طول.
- useFactory بينفّذ كود — تأكد إن المصدر موثوق.
- Request-scoped providers — أبطأ، صح، بس ضروريين لـ per-request state. متستخدمش singleton لـ user data.
Rate limiting و throttling
// @nestjs/throttler v5+
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [ThrottlerModule.forRoot([{ ttl: 60_000, limit: 100 }])],
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
// per-route override
@Throttle({ default: { ttl: 60_000, limit: 5 } })
@Post('login')
login(@Body() dto: LoginDto) { ... }
// IP source — احذر trust proxy
// throttler يستخدم req.ip؛ مع trust proxy خاطئ يستطيع المهاجم تزوير IPGraphQL في NestJS — surface هجوم خاصة
- Introspection — مقفول في prod افتراضياً، بس كتير بينساه. {introspection: false, playground: false}.
- Query depth & complexity — query عميقة بتوقّع السيرفر. استخدم graphql-depth-limit + graphql-query-complexity.
- Batching attacks — حد يبعت 1000 query في request واحد عشان يعمل brute force. حدّها من apollo-server options.
- BOLA على resolvers — كل resolver لازم يعيد فحص الـ ownership. متعتمدش على الـ parent resolver وخلاص.
- N+1 — استخدم DataLoader. غير كده، هتعمل DoS لنفسك ببلاش.
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import depthLimit from 'graphql-depth-limit';
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
introspection: process.env.NODE_ENV !== 'production',
playground: false,
validationRules: [
depthLimit(7),
createComplexityRule({
maximumComplexity: 1000,
estimators: [simpleEstimator({ defaultComplexity: 1 })],
}),
],
});WebSockets / Gateways
// خطر — gateway بدون auth
@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('msg')
handleMessage(client: Socket, payload: any) {
this.server.emit('msg', payload); // broadcast لكل user
}
}
// أي user متصل (حتى دون login) يستطيع broadcast
// PoC: socket.io-client ثم emit('msg', '<script>...</script>')
// الإصلاح
@UseGuards(WsJwtGuard) // Guard مخصّص للـ WS
@WebSocketGateway({ cors: { origin: ['https://app.target.gov'] } })
// JWT في handshake
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
socket.data.user = jwt.verify(token, secret);
next();
} catch { next(new Error('unauthorized')); }
});File Upload — Multer pitfalls
// خطر — يقبل أي ملف
@Post('avatar')
@UseInterceptors(FileInterceptor('file'))
upload(@UploadedFile() file: Express.Multer.File) {
fs.writeFileSync(`./uploads/${file.originalname}`, file.buffer);
// path traversal عبر originalname: "../../etc/cron.d/x"
}
// آمن
@Post('avatar')
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => cb(null, `${randomUUID()}.${extname(file.originalname).slice(1)}`),
}),
limits: { fileSize: 2 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (!['image/png','image/jpeg','image/webp'].includes(file.mimetype)) {
return cb(new BadRequestException('bad mime'), false);
}
cb(null, true);
},
}))
upload(@UploadedFile(new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 2 * 1024 * 1024 }),
new FileTypeValidator({ fileType: /^image\/(png|jpeg|webp)$/ }),
],
})) file: Express.Multer.File) { ... }
// + content-type magic byte check (file-type lib)
// + virus scan (clamav)
// + serve من CDN منفصل، ليس من نفس origin (يمنع HTML/JS upload XSS)ORM — TypeORM / Prisma injection
// TypeORM raw query — خطر
this.users.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// SQLi مباشر
// آمن — parameters
this.users.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
// أو QueryBuilder
this.users.createQueryBuilder('u').where('u.id = :id', { id }).getOne();
// Prisma — أساساً آمن، لكن $queryRawUnsafe خطر
prisma.$queryRawUnsafe(`SELECT * FROM ${table}`); // SQLi
prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`; // آمن (template tag)
// Prisma — extended raw filter بـ JSON
prisma.user.findMany({ where: req.body.where }); // مهاجم يضع OR / AND معقّدة
// قيّد الـ where لـ DTO معروفتحصين عام — main.ts كامل
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import helmet from 'helmet';
import compression from 'compression';
import cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
});
app.use(helmet());
app.use(compression());
app.use(cookieParser());
app.set('trust proxy', 1);
app.enableCors({
origin: ['https://app.target.gov'],
credentials: true,
methods: ['GET','POST','PUT','PATCH','DELETE'],
});
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
transform: true,
transformOptions: { enableImplicitConversion: false },
}));
app.enableVersioning({ type: VersioningType.URI });
app.setGlobalPrefix('api');
// graceful shutdown — هام لـ Kubernetes
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();مراجع و أدوات
- Snyk — مع Nest-aware rules.
- Semgrep — قواعد nestjs مخصصة (nestjs.audit).
- nest-cli + ESLint security plugin.
- OWASP API Security Top 10 — كل بند فيها بينطبق.
- Burp + Postman collection — جرّب كل endpoint بـ IDOR/BOLA.
- كتاب "NestJS in Practice" + قسم Security من docs.nestjs.com.
غلطات الـ junior في Nest
- ValidationPipe على controller واحد بس — والباقي مفتوح. اعملها global في main.ts.
- @UseGuards على method، مش على class — وبعدين بتضيف method جديد وتنسى الـ guard. اخلّيها على الـ class.
- JWT secret في @Module constructor — مش في Vault ولا KMS. بيتسرّب أول ما الـ source code يطلع.
- Custom Decorator بياخد user من request — من غير ما يتأكد إن الـ user authenticated. الـ decorator نفسه ممكن يكون الثغرة.
- RolesGuard بـ @SetMetadata('roles', ['admin']) — بس مفيش default deny. لو نسيت تحط الـ decorator، الـ endpoint مفتوح للكل.
- Exception filter بيرجّع stack trace — في production. الـ attacker شاكر.
الخلاصة الناشفة
NestJS مش "أكثر أماناً من Express" — هو بس "أكثر تنظيماً".
التنظيم بيدّيك مكان واحد تحط الحماية فيه (global pipes، global guards، global filters). بس لازم تحطها فعلاً.
الـ magic بتاع DI بيخفي الأخطاء، فمحدش بيلاحظها لحد ما تطلع breach في تويتر.
اكتبها على كشكولك:
Default deny. Whitelist everything. والـ class-validator على كل DTO. اوعى تستثني واحد.