Skip to content

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

Runtime security patterns that detect and block attacks from inside your Node.js application: SQL injection detection, path traversal and anomaly alerting.

4 min read
Runtime protection architecture showing application-level security hooks intercepting malicious requests before they reach business logic

Traditional security tools—WAFs, network firewalls, static analysis—operate outside your application. They see HTTP requests and network packets but lack the context of what the application actually does with the data. Runtime Application Self-Protection (RASP) embeds security checks inside the application itself, where it has full visibility into how inputs are processed, what database queries are built, and which files are accessed.

This doesn't replace perimeter security—it adds a critical inner layer that catches attacks that slip through external defenses.

SQL Injection Detection at the Query Layer

Instead of trying to sanitize inputs at the HTTP boundary, RASP monitors the actual queries being executed and flags suspicious patterns.

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);
    },
  };
}

The key insight is monitoring the final query string, not the input. Parameterized queries prevent injection by design, so any query containing injection patterns means either the application is building queries unsafely or an attacker found a path around the parameterization.

Path Traversal Prevention

File access operations are high-risk targets. RASP monitors filesystem calls to ensure they stay within allowed directories.

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();
}

Anomaly Detection for Runtime Behavior

RASP can detect unusual application behavior that might indicate an ongoing attack—sudden spikes in database queries, unusual endpoint access patterns, or unexpected error rates.

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;
}

Security Event Logging

RASP events need structured logging that feeds into security monitoring and alerting systems.

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(),
    }));
  }
}

Key Takeaways

Runtime Application Self-Protection adds security visibility where it matters most—inside the application, where you can see how inputs flow into queries, file operations, and system calls. Monitor database queries for injection patterns at the query construction layer rather than the HTTP input layer, because that's where the attack actually manifests. Guard filesystem operations by resolving paths and validating they stay within allowed directories. Track behavioral baselines to detect anomalous patterns like sudden query spikes or unusual error rates that indicate active exploitation. Log security events with structured data that feeds into alerting systems, escalating when repeated attack patterns emerge from the same source. RASP complements rather than replaces perimeter defenses—the combination of WAF, input validation, parameterized queries, and runtime monitoring creates defense in depth that no single layer can provide alone.

Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX