الفرق الحمراء — الهجومخبير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 خاطئ يستطيع المهاجم تزوير IP

GraphQL في 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.