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.

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.
// ❌ 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// ✅ 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.
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.
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.
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.


