الفرق الحمراء — الهجومخبير85mL68
أمن NestJS — Guards و Pipes و GraphQL
تجاوز Guards، ValidationPipe، DI poisoning، GraphQL في Nest
#NestJS#Guards#Pipes#GraphQL#DI
لماذا NestJS له فئة مخاطر خاصة
NestJS يضيف فوق Express/Fastify طبقات: Decorators، DI Container، Guards، Pipes، Interceptors، Modules. كل طبقة قد تُكتب بشكل خاطئ يفتح ثغرة لا توجد في Express العاري. الميزة هي نفسها العيب: الـ "magic" يخفي ما يجري فعلاً.
تشبيه — شرح مبسط
كمصفاة مياه متعددة المراحل. لو إحدى المراحل مكسورة و الباقي يعمل، يبدو الماء نظيفاً — لكنه ليس كذلك. NestJS pipeline مماثل: Guard ينجح، Pipe يفحص، Interceptor يعدّل — أي حلقة معطّلة تكسر السلسلة كلها.
تحذير قانوني
الأمثلة للتدريب في مختبرك. لا تختبر على إنتاج لا تملكه.
بنية الطلب — أين يمكن أن يفشل الفحص
text
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، المهاجم يحقن خصائص إضافية.
typescript
// خطر — 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
}));فخ transform
transform: true + enableImplicitConversion: true = خطر. "true" string يصبح boolean true. "123abc" قد يصبح 123. اضبط explicit conversion.
Mass assignment & DTO leakage
typescript
// طلب الـ 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 — أنماط شائعة
typescript
// 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)قواعد Guards
- افحص hasOwnProperty (أو استخدم Object.hasOwn) لا inherited.
- اعتمد على req.user الذي وضعه AuthGuard موثوق فقط.
- لا تستند على headers قابلة للتزوير.
- RolesGuard: استخدم Reflector لقراءة decorator، لا strings hard-coded.
- اختبر الـ Guard مع {__proto__: {isAdmin: true}} في request.
JWT في NestJS — فخاخ
typescript
// كثير من 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. لو attacker يستطيع تعديل provider في runtime (نادر لكن ممكن عبر debugger / unsafe eval)، يؤثر على كل الطلبات. لكن الأشهر:
- Custom providers بـ useValue من user input — لا تفعل ذلك أبداً.
- Dynamic modules تأخذ config من file — file injection يصبح RCE.
- useFactory ينفّذ كود — تأكد المصدر آمن.
- Request-scoped providers — أبطأ، لكن ضرورية لـ per-request state. لا تستخدم singleton لـ user data.
Rate limiting و throttling
typescript
// @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 بسيط.
typescript
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
typescript
// خطر — 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
typescript
// خطر — يقبل أي ملف
@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
typescript
// 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 كامل
typescript
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.