# Pentest Patterns - Vulnerability Patterns from Real Audits

> Module de l'agent `/hf-agent-security`
> **Source**: Rapports de pentest et audits de securite
> **Commandes**: `audit-ssrf`, `audit-dos`, `audit-pdf`, `audit-admin-tools`

---

## SSRF via Generateurs PDF

### Contexte

Les generateurs PDF (wkhtmltopdf, Puppeteer, Playwright) peuvent etre exploites pour des attaques SSRF si l'URL ou le contenu HTML est controle par l'utilisateur.

### Vecteurs d'Attaque

```
+---------------------------------------------------------------+
|                SSRF VIA PDF GENERATORS                         |
+---------------------------------------------------------------+
|                                                                |
|  SCENARIO D'ATTAQUE                                            |
|                                                                |
|  1. Attaquant injecte URL dans contenu HTML                   |
|     <iframe src="http://169.254.169.254/latest/meta-data/">   |
|                                                                |
|  2. Le serveur genere le PDF                                  |
|     wkhtmltopdf/Puppeteer fait la requete                     |
|                                                                |
|  3. Exfiltration de donnees                                   |
|     - AWS IMDS credentials                                    |
|     - Fichiers locaux (file:///)                              |
|     - Services internes                                       |
|                                                                |
|  PAYLOADS COURANTS                                             |
|                                                                |
|  AWS IMDS v1:                                                  |
|  <iframe src="http://169.254.169.254/latest/meta-data/">      |
|  <img src="http://169.254.169.254/latest/user-data">          |
|                                                                |
|  LFI:                                                          |
|  <iframe src="file:///etc/passwd">                            |
|  <script src="file:///app/.env"></script>                     |
|                                                                |
|  Services internes:                                            |
|  <iframe src="http://localhost:6379/">  (Redis)               |
|  <iframe src="http://internal-api:3000/admin">                |
|                                                                |
+---------------------------------------------------------------+
```

### Checklist SSRF PDF

```markdown
## Audit SSRF PDF Generators

### Configuration Generateur
- [ ] URLs externes bloquees dans le contenu HTML
- [ ] Protocole file:// desactive
- [ ] Reseau limite (pas d'acces IMDS/services internes)
- [ ] Timeout strict sur generation
- [ ] Sandbox/isolation du processus

### AWS IMDS Protection
- [ ] IMDSv2 active (hop-limit=1)
- [ ] Pas d'acces 169.254.169.254 depuis conteneurs
- [ ] IAM roles avec permissions minimales
- [ ] Rotation automatique des credentials

### Input Validation
- [ ] Sanitisation HTML strict
- [ ] Whitelist de domaines autorises
- [ ] Pas d'URLs utilisateur dans les templates
```

### Implementation Securisee (Puppeteer)

```typescript
// services/secure-pdf.service.ts
import puppeteer, { Browser, Page } from 'puppeteer';

@Injectable()
export class SecurePdfService {
  private browser: Browser;

  async onModuleInit() {
    this.browser = await puppeteer.launch({
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        // SECURITE: Bloquer acces reseau interne
        '--host-resolver-rules=MAP 169.254.169.254 127.0.0.1, MAP localhost 127.0.0.1',
        '--disable-web-security=false',
        // Bloquer protocoles dangereux
        '--disable-file-system',
      ],
    });
  }

  async generatePdf(htmlContent: string): Promise<Buffer> {
    // SECURITE: Sanitiser le HTML
    const sanitizedHtml = this.sanitizeHtml(htmlContent);

    const page = await this.browser.newPage();

    try {
      // SECURITE: Intercepter et bloquer les requetes dangereuses
      await page.setRequestInterception(true);
      page.on('request', (request) => {
        const url = request.url();

        // Bloquer IMDS, fichiers locaux, services internes
        if (
          url.includes('169.254.169.254') ||
          url.startsWith('file://') ||
          url.includes('localhost') ||
          url.includes('127.0.0.1') ||
          url.match(/^https?:\/\/10\./) ||
          url.match(/^https?:\/\/172\.(1[6-9]|2[0-9]|3[0-1])\./) ||
          url.match(/^https?:\/\/192\.168\./)
        ) {
          this.logger.warn(`Blocked request to: ${url}`);
          request.abort();
          return;
        }

        request.continue();
      });

      // SECURITE: Timeout strict
      await page.setContent(sanitizedHtml, {
        waitUntil: 'networkidle0',
        timeout: 10000, // 10 secondes max
      });

      const pdf = await page.pdf({
        format: 'A4',
        printBackground: true,
        timeout: 30000,
      });

      return Buffer.from(pdf);
    } finally {
      await page.close();
    }
  }

  private sanitizeHtml(html: string): string {
    // Supprimer les balises dangereuses
    const dangerous = [
      /<iframe[^>]*>/gi,
      /<object[^>]*>/gi,
      /<embed[^>]*>/gi,
      /<script[^>]*>[\s\S]*?<\/script>/gi,
      /javascript:/gi,
      /file:\/\//gi,
      /169\.254\.169\.254/g,
    ];

    let sanitized = html;
    for (const pattern of dangerous) {
      sanitized = sanitized.replace(pattern, '');
    }

    return sanitized;
  }
}
```

### Configuration AWS IMDSv2

```bash
# Activer IMDSv2 obligatoire sur les instances EC2
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1 \
  --http-endpoint enabled

# Terraform
resource "aws_instance" "app" {
  metadata_options {
    http_tokens                 = "required"
    http_put_response_hop_limit = 1
    http_endpoint               = "enabled"
  }
}
```

---

## DoS via Operations Couteuses

### Contexte

Les operations gourmandes en ressources (generation PDF, traitement d'images, exports volumineux) peuvent etre exploitees pour des attaques DoS.

### Checklist DoS Protection

```markdown
## Audit DoS Operations Couteuses

### Rate Limiting
- [ ] Throttling global configure
- [ ] Limites par endpoint sensible
- [ ] Limites par utilisateur/IP
- [ ] Backoff exponentiel

### Queue Processing
- [ ] Operations longues en queue (BullMQ)
- [ ] Timeout sur jobs
- [ ] Limite de jobs concurrents
- [ ] Dead letter queue

### Resource Limits
- [ ] Taille max fichiers upload
- [ ] Limite pages/elements par export
- [ ] Timeout generation PDF
- [ ] Memory limits sur conteneurs
```

### Implementation Rate Limiting (NestJS)

```typescript
// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot([
      {
        name: 'short',
        ttl: 1000,    // 1 seconde
        limit: 3,      // 3 requetes/seconde
      },
      {
        name: 'medium',
        ttl: 10000,   // 10 secondes
        limit: 20,     // 20 requetes/10s
      },
      {
        name: 'long',
        ttl: 60000,   // 1 minute
        limit: 100,    // 100 requetes/minute
      },
    ]),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

// Endpoints sensibles avec throttling strict
@Controller('exports')
export class ExportsController {
  @Post('pdf')
  @Throttle({ default: { limit: 5, ttl: 60000 } }) // 5 PDF/minute max
  async generatePdf(@Body() dto: ExportPdfDto) {
    // Ajouter a la queue au lieu d'executer directement
    return this.queue.add('generate-pdf', dto, {
      timeout: 30000,
      attempts: 1,
    });
  }
}
```

### Queue avec BullMQ

```typescript
// queues/pdf.processor.ts
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';

@Processor('pdf', {
  concurrency: 2, // Max 2 PDFs simultanes
  limiter: {
    max: 10,
    duration: 60000, // 10 jobs/minute max
  },
})
export class PdfProcessor extends WorkerHost {
  async process(job: Job): Promise<Buffer> {
    // Timeout automatique via BullMQ
    return this.pdfService.generate(job.data);
  }
}
```

---

## Path Traversal via Logique Metier

### Contexte

Les failles de path traversal peuvent exister meme avec une validation basique, si la logique metier permet de contourner les controles.

### Vecteurs d'Attaque

```
+---------------------------------------------------------------+
|              PATH TRAVERSAL VIA LOGIC FLAWS                    |
+---------------------------------------------------------------+
|                                                                |
|  SCENARIO: Switch/Case avec Default Permissif                  |
|                                                                |
|  function getFilePath(type: string, filename: string) {        |
|    let basePath: string;                                       |
|    switch(type) {                                              |
|      case 'avatar': basePath = '/uploads/avatars'; break;      |
|      case 'document': basePath = '/uploads/docs'; break;       |
|      default: basePath = '/'; // VULNERABLE!                   |
|    }                                                           |
|    return path.join(basePath, filename);                       |
|  }                                                             |
|                                                                |
|  EXPLOITATION:                                                 |
|  GET /files?type=invalid&filename=etc/passwd                   |
|  -> basePath = '/' (default case)                              |
|  -> path.join('/', 'etc/passwd') = '/etc/passwd'               |
|                                                                |
+---------------------------------------------------------------+
```

### Checklist Path Traversal

```markdown
## Audit Path Traversal

### Validation Chemins
- [ ] Whitelist de types/categories valides
- [ ] Pas de default permissif dans switch/case
- [ ] path.normalize() + verification prefix
- [ ] Bloquer sequences '../' et variantes encodees

### Architecture
- [ ] Fichiers hors webroot
- [ ] IDs opaques (UUID) au lieu de noms
- [ ] Mapping BDD entre ID et chemin reel
- [ ] Verification proprietaire avant acces
```

### Implementation Securisee

```typescript
// VULNERABLE
function getFilePath(type: string, filename: string): string {
  let basePath: string;
  switch(type) {
    case 'avatar': basePath = '/uploads/avatars'; break;
    case 'document': basePath = '/uploads/docs'; break;
    default: basePath = '/'; // DANGER!
  }
  return path.join(basePath, filename);
}

// SECURISE
const ALLOWED_TYPES = ['avatar', 'document', 'report'] as const;
type FileType = typeof ALLOWED_TYPES[number];

function getSecureFilePath(type: string, filename: string): string {
  // Whitelist stricte
  if (!ALLOWED_TYPES.includes(type as FileType)) {
    throw new BadRequestException('Invalid file type');
  }

  const basePaths: Record<FileType, string> = {
    avatar: '/var/app/uploads/avatars',
    document: '/var/app/uploads/docs',
    report: '/var/app/uploads/reports',
  };

  const basePath = basePaths[type as FileType];

  // Sanitiser le filename
  const sanitizedFilename = path.basename(filename); // Retire tout chemin

  const fullPath = path.join(basePath, sanitizedFilename);
  const normalizedPath = path.normalize(fullPath);

  // Verifier que le chemin reste dans le dossier autorise
  if (!normalizedPath.startsWith(basePath)) {
    throw new ForbiddenException('Path traversal detected');
  }

  return normalizedPath;
}
```

---

## Exposition d'Outils d'Administration

### Contexte

Des outils comme Adminer, PHPMyAdmin, ou des interfaces de debug peuvent etre exposes accidentellement en production ou preprod.

### Checklist Admin Tools

```markdown
## Audit Exposition Admin Tools

### Outils a Verifier
- [ ] Pas d'Adminer accessible
- [ ] Pas de PHPMyAdmin accessible
- [ ] Pas de pgAdmin accessible
- [ ] Pas de Redis Commander
- [ ] Pas de Kibana sans auth
- [ ] Pas de Grafana sans auth

### Ports Database
- [ ] MySQL (3306) non expose publiquement
- [ ] PostgreSQL (5432) non expose publiquement
- [ ] Redis (6379) non expose publiquement
- [ ] MongoDB (27017) non expose publiquement

### Debug/Dev Tools
- [ ] Pas de /debug ou /phpinfo
- [ ] Pas de stack traces en production
- [ ] Pas de Swagger sans auth en prod
```

### Scan Automatise

```bash
#!/bin/bash
# scripts/scan-admin-tools.sh

TARGET=$1
ADMIN_PATHS=(
  "/adminer"
  "/adminer.php"
  "/phpmyadmin"
  "/pma"
  "/pgadmin"
  "/redis"
  "/kibana"
  "/grafana"
  "/debug"
  "/phpinfo"
  "/phpinfo.php"
  "/swagger"
  "/api-docs"
  "/graphql"
  "/.env"
  "/config.json"
)

echo "Scanning $TARGET for exposed admin tools..."

for path in "${ADMIN_PATHS[@]}"; do
  status=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET$path")
  if [ "$status" != "404" ] && [ "$status" != "403" ]; then
    echo "[WARNING] $TARGET$path returned $status"
  fi
done

# Scan ports communs
echo "Scanning common database ports..."
nmap -p 3306,5432,6379,27017,9200,5601 "$TARGET" 2>/dev/null | grep -E "open|filtered"
```

---

## Messages d'Erreur Verbeux

### Contexte

Des messages d'erreur detailles peuvent reveler des informations sensibles (chemins, versions, stack traces).

### Checklist Error Handling

```markdown
## Audit Messages d'Erreur

### Production
- [ ] Pas de stack traces exposes
- [ ] Messages generiques pour le client
- [ ] Logging detaille cote serveur
- [ ] Pas de versions/chemins reveles

### Headers
- [ ] X-Powered-By supprime
- [ ] Server header minimal
- [ ] Pas d'infos framework
```

### Implementation NestJS

```typescript
// filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    // Log complet cote serveur
    this.logger.error({
      message: exception instanceof Error ? exception.message : 'Unknown error',
      stack: exception instanceof Error ? exception.stack : undefined,
      path: request.url,
      method: request.method,
      userId: request.user?.id,
      timestamp: new Date().toISOString(),
    });

    // Reponse generique au client en production
    const isProduction = process.env.NODE_ENV === 'production';

    const errorResponse = {
      statusCode: status,
      message: isProduction
        ? this.getGenericMessage(status)
        : exception instanceof Error ? exception.message : 'Unknown error',
      timestamp: new Date().toISOString(),
      path: request.url,
      // PAS de stack trace en production
      ...(isProduction ? {} : {
        stack: exception instanceof Error ? exception.stack : undefined
      }),
    };

    response.status(status).json(errorResponse);
  }

  private getGenericMessage(status: number): string {
    const messages: Record<number, string> = {
      400: 'Bad request',
      401: 'Unauthorized',
      403: 'Forbidden',
      404: 'Not found',
      500: 'Internal server error',
    };
    return messages[status] || 'An error occurred';
  }
}
```

---

## Integration CI/CD

```yaml
# .github/workflows/security-pentest-patterns.yml
name: Pentest Patterns Check

on: [push, pull_request]

jobs:
  ssrf-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check for SSRF-vulnerable patterns
        run: |
          # Verifier absence de patterns SSRF
          ! grep -r "169.254.169.254" src/ || exit 1
          ! grep -rE "wkhtmltopdf|puppeteer" src/ --include="*.ts" | grep -v "sanitize" || echo "WARNING: PDF generator without sanitization"

      - name: Check for verbose error handling
        run: |
          # Verifier que les stack traces ne sont pas exposes
          grep -rE "stack.*response|response.*stack" src/ && exit 1 || true

      - name: Check admin tools exposure
        run: |
          # Verifier absence d'outils admin dans docker-compose
          ! grep -iE "adminer|phpmyadmin|pgadmin" docker-compose*.yml || echo "WARNING: Admin tool in compose"
```

---

## References

- [OWASP SSRF Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)
- [AWS IMDSv2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)
- [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)
- [BullMQ](https://docs.bullmq.io/)
- [CWE-918: SSRF](https://cwe.mitre.org/data/definitions/918.html)
