Zum Inhalt springen

Runtime Application Self-Protection (RASP) in Node.js

Laufzeit-Sicherheitsmuster, die Angriffe in deiner Node.js-Anwendung erkennen und blockieren: SQL-Injection, Path Traversal und Anomalie-Alarme.

4 Min. Lesezeit
Architektur des Laufzeitschutzes, die zeigt, wie Sicherheits-Hooks auf Anwendungsebene bösartige Anfragen abfangen, bevor sie die Geschäftslogik erreichen

Klassische Sicherheitswerkzeuge – WAFs, Netzwerk-Firewalls, statische Analyse – arbeiten außerhalb Ihrer Anwendung. Sie sehen HTTP-Anfragen und Netzwerkpakete, kennen aber nicht den Kontext dessen, was die Anwendung tatsächlich mit den Daten macht. Runtime Application Self-Protection (RASP) verankert Sicherheitsprüfungen direkt in der Anwendung selbst, wo sie vollständige Einsicht darin hat, wie Eingaben verarbeitet werden, welche Datenbankabfragen aufgebaut werden und auf welche Dateien zugegriffen wird.

Das ersetzt keine Perimeter-Sicherheit, sondern ergänzt sie um eine entscheidende innere Schicht, die Angriffe abfängt, die durch die äußeren Verteidigungslinien schlüpfen.

SQL-Injection-Erkennung auf Ebene der Abfragen

Statt zu versuchen, Eingaben an der HTTP-Grenze zu bereinigen, überwacht RASP die tatsächlich ausgeführten Abfragen und markiert verdächtige Muster.

tstypescript
// ❌ Relying only on input validation at the HTTP layer
function validateInput(input: string): boolean {
  // Blocklist approach — always incomplete
  const suspicious = /('|--|;|DROP|UNION|SELECT)/i;
  return !suspicious.test(input);
}
// Bypassed by: URL encoding, Unicode tricks, comment injection
tstypescript
// ✅ RASP: Monitor query construction for injection signatures
interface QueryAnalysis {
  query: string;
  isSuspicious: boolean;
  reason?: string;
  severity: "low" | "medium" | "high" | "critical";
}
 
class SQLInjectionDetector {
  private readonly patterns: { pattern: RegExp; severity: string; description: string }[] = [
    {
      pattern: /UNION\s+(?:ALL\s+)?SELECT/i,
      severity: "critical",
      description: "UNION-based injection attempt",
    },
    {
      pattern: /;\s*(?:DROP|ALTER|TRUNCATE|DELETE\s+FROM)\s/i,
      severity: "critical",
      description: "Destructive statement appended",
    },
    {
      pattern: /OR\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/i,
      severity: "high",
      description: "Tautology-based authentication bypass",
    },
    {
      pattern: /SLEEP\s*\(\s*\d+\s*\)/i,
      severity: "high",
      description: "Time-based blind injection",
    },
    {
      pattern: /BENCHMARK\s*\(/i,
      severity: "high",
      description: "Benchmark-based blind injection",
    },
  ];
 
  analyze(query: string): QueryAnalysis {
    for (const { pattern, severity, description } of this.patterns) {
      if (pattern.test(query)) {
        return {
          query,
          isSuspicious: true,
          reason: description,
          severity: severity as QueryAnalysis["severity"],
        };
      }
    }
 
    return { query, isSuspicious: false, severity: "low" };
  }
}
 
// Wrap database client to intercept queries
function createProtectedClient(
  client: DatabaseClient,
  detector: SQLInjectionDetector,
  onThreat: (analysis: QueryAnalysis) => void
): DatabaseClient {
  return {
    ...client,
    async query(queryText: string, params?: unknown[]) {
      const analysis = detector.analyze(queryText);
 
      if (analysis.isSuspicious) {
        onThreat(analysis);
 
        if (
          analysis.severity === "critical" ||
          analysis.severity === "high"
        ) {
          throw new SecurityError(
            `Blocked suspicious query: ${analysis.reason}`
          );
        }
      }
 
      return client.query(queryText, params);
    },
  };
}

Der entscheidende Punkt ist, den finalen Abfrage-String zu überwachen, nicht die Eingabe. Parametrisierte Abfragen verhindern Injection bereits durch ihr Design, sodass jede Abfrage mit Injection-Mustern entweder bedeutet, dass die Anwendung Abfragen unsicher zusammensetzt, oder dass ein Angreifer einen Weg um die Parametrisierung herum gefunden hat.

Abwehr von Path Traversal

Dateizugriffe sind hochriskante Angriffsziele. RASP überwacht Dateisystemaufrufe, um sicherzustellen, dass sie innerhalb der erlaubten Verzeichnisse bleiben.

tstypescript
import { resolve, relative } from "path";
 
interface FileAccessPolicy {
  allowedRoots: string[];
  blockedExtensions: string[];
  maxPathLength: number;
}
 
class PathTraversalGuard {
  private readonly policy: FileAccessPolicy;
  private readonly resolvedRoots: string[];
 
  constructor(policy: FileAccessPolicy) {
    this.policy = policy;
    this.resolvedRoots = policy.allowedRoots.map(r => resolve(r));
  }
 
  validatePath(requestedPath: string): {
    allowed: boolean;
    resolvedPath: string;
    reason?: string;
  } {
    // Resolve to absolute path to eliminate ../
    const resolved = resolve(requestedPath);
 
    // Check path length
    if (resolved.length > this.policy.maxPathLength) {
      return {
        allowed: false,
        resolvedPath: resolved,
        reason: "Path exceeds maximum length",
      };
    }
 
    // Check if path is within allowed directories
    const withinAllowed = this.resolvedRoots.some(root => {
      const rel = relative(root, resolved);
      return !rel.startsWith("..") && !resolve(rel).startsWith("..");
    });
 
    if (!withinAllowed) {
      return {
        allowed: false,
        resolvedPath: resolved,
        reason: `Path ${resolved} is outside allowed directories`,
      };
    }
 
    // Check blocked extensions
    const ext = resolved.split(".").pop()?.toLowerCase() ?? "";
    if (this.policy.blockedExtensions.includes(`.${ext}`)) {
      return {
        allowed: false,
        resolvedPath: resolved,
        reason: `File extension .${ext} is blocked`,
      };
    }
 
    return { allowed: true, resolvedPath: resolved };
  }
}
 
const guard = new PathTraversalGuard({
  allowedRoots: ["/app/uploads", "/app/public"],
  blockedExtensions: [".env", ".key", ".pem", ".sh"],
  maxPathLength: 512,
});
 
// Usage in file serving middleware
function secureFileMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const result = guard.validatePath(req.params.filepath);
 
  if (!result.allowed) {
    logSecurityEvent({
      type: "path_traversal_attempt",
      path: req.params.filepath,
      resolvedPath: result.resolvedPath,
      reason: result.reason,
      ip: req.ip,
      timestamp: new Date(),
    });
 
    return res.status(403).json({ error: "Access denied" });
  }
 
  req.resolvedFilePath = result.resolvedPath;
  next();
}

Anomalieerkennung für das Laufzeitverhalten

RASP kann ungewöhnliches Anwendungsverhalten erkennen, das auf einen laufenden Angriff hindeuten könnte – plötzliche Spitzen bei Datenbankabfragen, ungewöhnliche Zugriffsmuster auf Endpunkte oder unerwartete Fehlerraten.

tstypescript
interface BehaviorBaseline {
  endpoint: string;
  avgQueriesPerRequest: number;
  avgResponseTime: number;
  avgErrorRate: number;
  stdDevQueries: number;
  stdDevResponseTime: number;
}
 
class BehaviorMonitor {
  private baselines: Map<string, BehaviorBaseline> = new Map();
  private windowMs: number = 60_000; // 1-minute windows
  private currentWindow: Map<string, RequestMetrics[]> = new Map();
 
  recordRequest(endpoint: string, metrics: RequestMetrics): void {
    const requests = this.currentWindow.get(endpoint) ?? [];
    requests.push(metrics);
    this.currentWindow.set(endpoint, requests);
  }
 
  detectAnomalies(
    endpoint: string,
    metrics: RequestMetrics
  ): AnomalyReport | null {
    const baseline = this.baselines.get(endpoint);
    if (!baseline) return null;
 
    const anomalies: string[] = [];
 
    // Detect unusual number of database queries
    if (
      metrics.queryCount >
      baseline.avgQueriesPerRequest + 3 * baseline.stdDevQueries
    ) {
      anomalies.push(
        `Unusual query count: ${metrics.queryCount} ` +
        `(baseline: ${baseline.avgQueriesPerRequest.toFixed(1)})`
      );
    }
 
    // Detect unusual response time
    if (
      metrics.responseTime >
      baseline.avgResponseTime + 3 * baseline.stdDevResponseTime
    ) {
      anomalies.push(
        `Unusual response time: ${metrics.responseTime}ms ` +
        `(baseline: ${baseline.avgResponseTime.toFixed(0)}ms)`
      );
    }
 
    if (anomalies.length === 0) return null;
 
    return {
      endpoint,
      timestamp: new Date(),
      anomalies,
      metrics,
      baseline,
    };
  }
}
 
interface RequestMetrics {
  queryCount: number;
  responseTime: number;
  statusCode: number;
  timestamp: Date;
}
 
interface AnomalyReport {
  endpoint: string;
  timestamp: Date;
  anomalies: string[];
  metrics: RequestMetrics;
  baseline: BehaviorBaseline;
}

Protokollierung von Sicherheitsereignissen

RASP-Ereignisse benötigen ein strukturiertes Logging, das in Systeme zur Sicherheitsüberwachung und Alarmierung einfließt.

tstypescript
interface SecurityEvent {
  type: string;
  severity: "info" | "warning" | "critical";
  timestamp: Date;
  source: {
    ip: string;
    userAgent?: string;
    userId?: string;
  };
  details: Record<string, unknown>;
  action: "logged" | "blocked" | "alerted";
}
 
class SecurityEventLogger {
  private events: SecurityEvent[] = [];
  private alertThresholds: Map<string, number> = new Map([
    ["sql_injection_attempt", 3],
    ["path_traversal_attempt", 5],
    ["brute_force_attempt", 10],
  ]);
 
  private recentCounts: Map<string, number> = new Map();
 
  log(event: SecurityEvent): void {
    this.events.push(event);
 
    // Track event frequency per type+IP
    const key = `${event.type}:${event.source.ip}`;
    const count = (this.recentCounts.get(key) ?? 0) + 1;
    this.recentCounts.set(key, count);
 
    const threshold = this.alertThresholds.get(event.type);
    if (threshold && count >= threshold) {
      this.escalate(event, count);
    }
 
    // Emit structured log
    console.log(JSON.stringify({
      level: "security",
      type: event.type,
      severity: event.severity,
      action: event.action,
      ip: event.source.ip,
      userId: event.source.userId,
      details: event.details,
      timestamp: event.timestamp.toISOString(),
    }));
  }
 
  private escalate(event: SecurityEvent, count: number): void {
    console.log(JSON.stringify({
      level: "security_alert",
      message: `Repeated ${event.type} from ${event.source.ip}: ${count} occurrences`,
      type: event.type,
      ip: event.source.ip,
      count,
      timestamp: new Date().toISOString(),
    }));
  }
}

Die wichtigsten Erkenntnisse

Runtime Application Self-Protection schafft Sicherheitstransparenz genau dort, wo sie am meisten zählt – innerhalb der Anwendung, wo Sie sehen können, wie Eingaben in Abfragen, Dateioperationen und Systemaufrufe einfließen. Überwachen Sie Datenbankabfragen auf Injection-Muster auf der Ebene der Abfrageerstellung statt auf der Ebene der HTTP-Eingabe, denn genau dort zeigt sich der Angriff tatsächlich. Schützen Sie Dateisystemoperationen, indem Sie Pfade auflösen und sicherstellen, dass sie innerhalb der erlaubten Verzeichnisse bleiben. Verfolgen Sie Verhaltensbaselines, um anomale Muster wie plötzliche Abfragespitzen oder ungewöhnliche Fehlerraten zu erkennen, die auf eine aktive Ausnutzung hindeuten. Protokollieren Sie Sicherheitsereignisse mit strukturierten Daten, die in Alarmsysteme einfließen, und eskalieren Sie, wenn wiederholte Angriffsmuster von derselben Quelle auftreten. RASP ergänzt Perimeter-Verteidigungen, statt sie zu ersetzen – die Kombination aus WAF, Eingabevalidierung, parametrisierten Abfragen und Laufzeitüberwachung schafft eine Tiefenverteidigung, die keine einzelne Schicht allein bieten kann.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX