Express middleware that scores every inbound IP with the WhoisFreaks IP Reputation API and responds by threat level instead of a flat block/allow.
A request comes in, the middleware looks up the IP's reputation, reads the threat_score (0–100), and picks one of four actions:
| Score | Action | What happens |
|---|---|---|
| 0–25 | allow |
request passes through |
| 26–50 | challenge |
CAPTCHA / rate limit (your handler) |
| 51–75 | step_up |
require extra auth (your handler) |
| 76–100 | block |
403 |
Results are cached, private/localhost IPs are skipped, and the check fails open so a reputation-API outage never takes your app down.
git clone https://github.com/WhoisFreaks/ip-reputation-middleware
cd ip-reputation-middleware
npm install
cp .env.example .env # paste your API key
npm startGet a free key (500 credits, no card) at https://billing.whoisfreaks.com/signup. Each IP lookup costs one credit. The .env file is loaded automatically via dotenv, so npm start picks up your key with no extra flags.
import "dotenv/config";
import express from "express";
import { ipReputation } from "./src/ipReputation.js";
const app = express();
app.set("trust proxy", true); // so req.ip reflects the real client behind a proxy
app.use(ipReputation({
failOpen: true,
onBlock: (req, res) => res.status(403).json({ error: "Access denied" }),
onChallenge: (req, res, next) => { res.set("X-Require-Challenge", "1"); next(); },
onStepUp: (req, res, next) => { res.set("X-Require-MFA", "1"); next(); },
}));Downstream handlers can read the raw flags off req.ipReputation (threat_score, is_vpn, is_proxy, is_tor, is_bot, is_known_attacker, and more).
| Option | Default | Description |
|---|---|---|
failOpen |
true |
Allow the request if the API errors. Set false to 503. |
onBlock |
— | Handler for scores 76–100. Defaults to 403. |
onChallenge |
— | Handler for scores 26–50. Defaults to next(). |
onStepUp |
— | Handler for scores 51–75. Defaults to next(). |
Score a file of IPs offline (one per line) to build an edge blocklist:
node enrich-logs.js sample-ips.txtnpm testCovers the tier boundaries in decide() — the logic that decides whether a request is allowed, challenged, or blocked.
- Node 18+ required (uses the built-in
fetch). app.set("trust proxy", ...): usetrueonly when every request genuinely passes through a proxy you control. If your app is also reachable directly, set the exact number of hops instead, or clients can spoofX-Forwarded-For.- The in-memory cache is fine for a single process. Swap in Redis when you run more than one.
MIT