DEV Community

Cover image for How I Built Security Middleware for Express: SQL Injection, XSS, Rate Limiting, and IP Reputation
Sandeep Sharma
Sandeep Sharma

Posted on

How I Built Security Middleware for Express: SQL Injection, XSS, Rate Limiting, and IP Reputation

How I Built Security Middleware for Express: SQL Injection, XSS, Rate Limiting, and IP Reputation

Security in an Express application can quickly become a collection of separate middleware packages and configuration rules.

I wanted to experiment with a different approach: a single middleware that evaluates incoming requests using multiple detection rules and assigns a risk score rather than making every security decision as a simple yes/no check.

That experiment became SecurityWatch.

What is SecurityWatch?

SecurityWatch is score-based runtime security middleware for Express.

It currently detects and evaluates:

  • SQL injection patterns
  • XSS patterns
  • Brute-force behavior
  • Rate-limit abuse
  • Suspicious request behavior
  • Payload anomalies
  • IP reputation

The project is available on NPM:

https://www.npmjs.com/package/securitywatch

The source code is on GitHub:

https://github.com/sandeepsharmacode/securitywatch

Installation

SecurityWatch requires:

  • Node.js 18 or newer
  • Express 5 or newer

Install it with:

npm install securitywatch
Enter fullscreen mode Exit fullscreen mode

Then add it to an Express application:

import express from "express";
import { securityWatch } from "securitywatch";

const app = express();

app.use(securityWatch());

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

That's the basic setup.

How the scoring system works

Instead of treating every suspicious pattern as an automatic block, SecurityWatch gives each detection rule a numeric score.

The scores are combined and then evaluated against configurable thresholds.

The default behavior is:

Score Action
0–4 Allow
5–9 Warn
10–14 Throttle with HTTP 429
15+ Block with HTTP 403

For example, a request might contain a suspicious pattern that isn't serious enough on its own to block.

Rather than immediately rejecting it, the middleware can assign a warning-level score.

This gives the application more room to distinguish between suspicious activity and clearly malicious behavior.

Route sensitivity

Not every endpoint has the same security requirements.

A public search endpoint and an administrator endpoint shouldn't necessarily have identical thresholds.

SecurityWatch therefore supports route sensitivity:

routeSensitivity: {
  "/admin": "critical",
  "/login": "high",
  "/search": "low",
}
Enter fullscreen mode Exit fullscreen mode

The available levels are:

  • low — 0.5×
  • medium — 1×
  • high — 1.5×
  • critical — 2×

This allows the same detection rules to have different effective sensitivity depending on the route.

Configuring the middleware

A more complete configuration can look like this:

app.use(securityWatch({
  sqlInjection: true,
  xss: true,

  bruteForce: {
    maxAttempts: 5,
    windowMs: 5 * 60_000,
    blockDurationMs: 15 * 60_000,
    authRoutes: ["/login", "/auth"],
  },

  rateLimit: {
    windowMs: 60_000,
    maxRequests: 100,
    routes: {
      "/login": 5,
      "/api": 60,
    },
  },

  suspiciousBehavior: true,
  payloadAnomaly: true,
  ipReputation: true,

  routeSensitivity: {
    "/admin": "critical",
    "/login": "high",
    "/search": "low",
  },

  thresholds: {
    warn: 5,
    throttle: 10,
    block: 15,
  },

  whitelist: ["127.0.0.1"],

  trustProxy: false,

  onBlock: (req, info) => {
    console.log(`Blocked: ${info.ip}`);
  },

  onWarn: (req, info) => {
    console.log(`Warning: ${info.ip}`);
  },
}));
Enter fullscreen mode Exit fullscreen mode

Why I chose scoring instead of binary blocking

A simple security rule might look like:

suspicious pattern detected → block request

The problem is that real application traffic isn't always that simple.

A request can contain something unusual without necessarily being an attack.

A scoring system allows multiple signals to contribute to the final decision.

For example:

Rule A → +3
Rule B → +2
Route sensitivity → applied
-------------------------
Final score → evaluated against thresholds
Enter fullscreen mode Exit fullscreen mode

The intention is to reduce unnecessary blocking while still allowing clearly suspicious requests to cross a stronger threshold.

Brute-force protection

SecurityWatch can also track repeated authentication attempts.

For example:

bruteForce: {
  maxAttempts: 5,
  windowMs: 5 * 60_000,
  blockDurationMs: 15 * 60_000,
  authRoutes: ["/login", "/auth"],
}
Enter fullscreen mode Exit fullscreen mode

This allows authentication routes to have their own attempt limits and temporary blocking behavior.

Rate limiting

Rate limiting can be configured globally and for specific routes:

rateLimit: {
  windowMs: 60_000,
  maxRequests: 100,
  routes: {
    "/login": 5,
    "/api": 60,
  },
}
Enter fullscreen mode Exit fullscreen mode

This is useful when sensitive endpoints need stricter limits than ordinary API traffic.

Security considerations

Because this is security middleware, there are several implementation details worth mentioning.

Input is truncated before scanning, with a maximum of 20,000 characters.

The detection regexes use bounded quantifiers to help prevent problematic behavior from excessively large inputs.

The middleware also limits its in-memory tracking:

  • IP tracking is capped at 10,000 entries.
  • Route tracking is capped at 100 routes per IP.
  • Rate-limit keys are normalized.

By default, X-Forwarded-For isn't trusted.

If the application is behind a trusted reverse proxy, trustProxy can be enabled:

trustProxy: true
Enter fullscreen mode Exit fullscreen mode

This should only be enabled when the proxy configuration is actually trusted.

Alerts

SecurityWatch can also expose security events through callbacks such as:

onBlock: (req, info) => {
  console.log(`Blocked: ${info.ip}`);
},

onWarn: (req, info) => {
  console.log(`Warning: ${info.ip}`);
},
Enter fullscreen mode Exit fullscreen mode

The configuration also supports console alerts and Slack webhook configuration.

Fail-open behavior

One design decision I made was to catch internal middleware errors and allow the request to proceed while logging the error.

That means an internal SecurityWatch failure should not automatically become an application-wide outage.

This is a deliberate trade-off and is something I expect to continue evaluating as the project develops.

Is this a replacement for every security package?

No.

SecurityWatch is an experiment in combining runtime request detection and abuse controls into one Express middleware.

It shouldn't be treated as a replacement for secure application design, parameterized database queries, output encoding, authentication, authorization, HTTPS, secure headers, dependency management, or other appropriate security controls.

In particular, SQL injection should still be prevented at the database/query layer rather than relying solely on pattern detection.

What's next?

The project is still young, and I'm particularly interested in improving:

  • Detection accuracy
  • False-positive handling
  • Performance
  • Rule quality
  • Configuration ergonomics
  • Testing against realistic application traffic

If you're an Express or Node.js developer, I'd be interested in hearing how you'd approach the architecture and what security cases you think are missing.

SecurityWatch:

NPM: https://www.npmjs.com/package/securitywatch

GitHub: https://github.com/sandeepsharmacode/securitywatch

Website: https://securitywatch.sandeepsharmadev.in

Top comments (0)