Skip to content

achmdfzn/hippocrates

Repository files navigation

Hippocrates

npm version CI codecov License GitHub Stars

Next.js App Router security middleware that routes malicious requests to a decoy handler instead of blocking them. Uses Redis-backed cumulative threat scoring across six detection layers.

npm install hippocrates-middleware zod

Contents


How It Works

Every incoming request passes through detection layers. Each layer can add points to an IP's cumulative threat score stored in Redis. If the score crosses the threshold, the request is routed to a decoy generator instead of the real handler. The decoy returns a 200 OK with fake data. The caller never sees a 403 or 429 and gets no signal they were detected.

Incoming Request
      |
      v
 L-1  Allowlist? ---- YES --> Forward to handler (skip all checks)
      | NO
      v
 L0   Pre-flight score check ---- score >= threshold? ---- YES --> HONEYPOT (200 OK, fake data)
      | NO
      v
 Pre-body analyzers: L1 Timing, L2 Velocity, L3 UA, L4 Headers
      | score >= threshold?
      |-- YES --> HONEYPOT
      | NO
      v
 Body parsing + Post-body analyzers: L5 Obfuscation, L6 Schema
      | score >= threshold?
      |-- YES --> HONEYPOT
      | NO
      v
 Forward clean request to real handler

Requirements

  • Node.js >= 18
  • Next.js >= 14 (peer dependency)
  • Zod >= 3.22 (peer dependency)
  • Redis client (Upstash, ioredis, or compatible)

Tutorial

This tutorial walks through protecting a Next.js App Router endpoint from scratch.

1. Create or open a Next.js project

npx create-next-app@latest my-app --typescript
cd my-app

2. Install dependencies

npm install hippocrates-middleware zod
npm install @upstash/redis   # or your Redis client of choice

3. Set up a route

Create app/api/users/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { Redis } from "@upstash/redis";
import { withHippocrates, z } from "hippocrates-middleware";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// Schemas must use .strict() -- extra fields trigger a violation
const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
}).strict();

async function handler(req: NextRequest): Promise<NextResponse> {
  const body = await req.json();
  // body is already validated by the middleware
  return NextResponse.json({ id: crypto.randomUUID(), ...body });
}

export const POST = withHippocrates(handler, CreateUserSchema, redis);

4. Add environment variables

Create .env.local:

UPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token

5. Run

npm run dev

Send a valid request:

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'
# Response: 200 OK with { id, name, email }

The middleware validates the body against the Zod schema. If the body is valid, it passes through to handler.

6. Trigger the honeypot

Send a request with extra fields:

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "role": "admin"}'

The .strict() schema rejects unknown fields. The request is routed to the decoy generator and you receive a 200 OK with fake data.

Send too many requests from the same IP quickly, or use a tool with a suspicious User-Agent:

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -H "User-Agent: python-requests/2.31.0" \
  -d '{"name": "Bob", "email": "bob@example.com"}'

The UA pattern python-requests triggers L3. Repeat this 10+ times within 10 seconds and L2 velocity tracking adds more points. Cross the threshold and you get the honeypot.

7. With custom configuration

export const POST = withHippocrates(handler, CreateUserSchema, redis, {
  preset: "strict",
  allowlist: { ips: ["10.0.0.0/8", "127.0.0.1"] },
  bodyLimit: { maxBytes: 524288, enabled: true },
  scoring: {
    impossibleTiming: 35,
    suspiciousUserAgent: 25,
  },
  hooks: {
    onHoneypot: (event) => {
      console.log(`Honeypot served to ${event.ip}`);
    },
  },
});

Codebase Structure

src/
  index.ts                     # Entry point -- withHippocrates() HOF, re-exports
  engine/
    types.ts                   # Type definitions (RedisClient, HippocratesConfig, etc.)
    constants.ts               # Default values, UA patterns, obfuscation patterns
    analyzers.ts               # Built-in analyzer plugin placeholders (L1-L6)
    threat-score-engine.ts     # Redis-backed scoring engine with circuit breaker
  system/
    pipeline.ts                # Request processing pipeline
    honeypot.ts                # Decoy response generator
    validator.ts               # Zod validation helpers (validatePayload, ensureStrict)
  plugins/
    ml-engine.ts               # Python sidecar AnalyzerPlugin
  utils/
    ip.ts                      # IPv6 normalization and client IP resolution
  __tests__/
    helpers.ts                 # Test mocks
    ip.test.ts                 # 30 tests
    threat-score-engine.test.ts # 45 tests
    validate-payload.test.ts   # 8 tests
    decoy.test.ts              # 11 tests
	    with-hippocrates.test.ts   # 58 tests (integration)
	    ensure-strict.test.ts      # 27 tests
    redis-degradation.test.ts  # 6 tests
    stats.test.ts              # 5 tests
    stats-integration.test.ts  # 13 tests
	    ml-engine-integration.test.ts # 17 tests
engine-python/
  app/
    main.py                    # FastAPI application
    config.py                  # Environment-based settings
    models.py                  # Request/response models
    analyzers/                 # ML detection modules
      prompt_injection.py
      obfuscation_advanced.py
      content_risk.py
  tests/
    test_analyzers.py          # 31 tests
    test_api.py                # 8 tests
  Dockerfile
  requirements.txt
example/
  app/api/data/route.ts        # Reference implementation

Pairing the Repo

git clone https://github.com/achmdfzn/hippocrates.git
cd hippocrates

npm install
npm run build
npm test                       # 220 tests
npm run typecheck              # tsc --noEmit, zero errors
npm run lint                   # ESLint, zero errors

To run the full stack with the Python ML engine:

	# Requires: Docker, Python 3.12+
	docker compose up -d           # Redis + ML engine
	npm run test:all               # TS tests (220) + Python tests (39)

Development workflow:

npm run dev                    # tsup --watch (recompiles on source changes)
npm run test:watch             # Vitest watch (reruns tests on changes)

CI pipeline (GitHub Actions):

quality (Node 18/20/22): lint -> typecheck -> test -> coverage -> build
python-tests:               pip install -> pytest (31 analyzer + 8 API tests)
docker:                     build ML engine image -> healthcheck

Defense Layers

Layer Check Points Condition
L-1 IP allowlist 0 (bypass all) IP is in allowlist config
L0 Pre-flight score Instant honeypot Existing Redis score >= threshold
L1 Request timing +25 Interval < 50ms
L2 Request velocity +40 Burst > 15 req / 10s window
L3 User-Agent +15 Suspicious or missing UA
L4 HTTP headers +15 Missing or wildcard Accept, etc.
L5 Payload obfuscation +100 Base64, hex, URL encoding, Unicode
L6 Zod schema +100 .strict() violation

L5 and L6 push the score to 100 immediately on detection.

UA patterns (40+): LLM SDKs (anthropic-sdk, openai-node, langchain), HTTP libs (python-requests, curl, axios), browser automation (playwright, puppeteer), 2026 AI agents (claude, cursor, perplexitybot, opencode).

Obfuscation patterns: Base64 (>=24 chars), hex encoding (>=16 chars), URL encoding (5+ consecutive), Unicode escapes, HTML entities.


Configuration

interface HippocratesConfig {
  preset?: "strict" | "moderate" | "relaxed";
  threatScoreThreshold?: number;          // Default: 65
  velocityWindowMs?: number;              // Default: 10000
  velocityMaxRequests?: number;           // Default: 15
  threatTtlSeconds?: number;              // Default: 3600
  scoring?: Partial<ThreatScoringWeights>;
  decoyGenerator?: (req: NextRequest) => Record<string, unknown>;
  debugMode?: boolean;                    // Default: false
  plugins?: AnalyzerPlugin[];
  hooks?: HippocratesHooks;
  allowlist?: { ips: string[] };
  bodyLimit?: { maxBytes: number; enabled: boolean };
  methodThresholds?: Partial<Record<string, number>>;
  violationMessages?: Record<string, (violation: string) => Record<string, unknown>>;
  statsTracker?: StatsTracker;
}

Preset values:

Preset Threshold Velocity Max Window
strict 40 10 req 10s
moderate 65 15 req 10s
relaxed 80 30 req 30s

Redis key layout:

Key Purpose TTL
hc:s:{ip} Threat score (0-100) threatTtlSeconds
hc:t:{ip} Request timestamps (velocity) windowMs + 10s
hc:l:{ip} Last-seen timestamp (timing) 300s

Custom violation messages:

export const POST = withHippocrates(handler, schema, redis, {
  violationMessages: {
    obfuscation: (violation) => ({
      error: "invalid_payload_format",
      code: "OBFUSCATION_DETECTED",
    }),
    schema: (violation) => ({
      error: "validation_failed",
    }),
  },
});

The key is the violation type prefix (obfuscation, schema, ua, velocity, timing, header). The function receives the full violation tag string and returns an object that merges with the decoy response.


Plugin System

Implement custom detection logic with the AnalyzerPlugin interface:

import { type AnalyzerPlugin } from "hippocrates-middleware";

const geoBlock: AnalyzerPlugin = {
  name: "geo_block",
  phase: "pre-body",      // "pre-body" | "post-body"
  priority: 50,           // Lower runs first. Default: 100
  analyze(req, ctx) {
    const country = req.headers.get("x-country");
    if (country === "blocked") {
      return { score: 50, tags: ["geo:blocked"] };
    }
    return { score: 0, tags: [] };
  },
};

export const POST = withHippocrates(handler, schema, redis, {
  plugins: [geoBlock],
});

Plugins sorted by priority ascending within each phase. Same priority preserves registration order.

Additional use-case examples:

Rate-limit mimic — add score on high request frequency without blocking:

const rateMimic: AnalyzerPlugin = {
  name: "rate_mimic",
  phase: "pre-body",
  priority: 90,
  analyze(req, ctx) {
    const freq = parseInt(req.headers.get("x-request-frequency") ?? "0");
    if (freq > 100) return { score: 30, tags: ["rate:high"] };
    if (freq > 50)  return { score: 15, tags: ["rate:medium"] };
    return { score: 0, tags: [] };
  },
};

Known scraper detection — match URL patterns for specific routes:

const scraperDetect: AnalyzerPlugin = {
  name: "scraper_detect",
  phase: "pre-body",
  priority: 40,
  analyze(req, ctx) {
    const url = req.nextUrl.pathname;
    const sensitivePaths = ["/api/users", "/api/orders", "/api/admin"];
    if (sensitivePaths.some((p) => url.startsWith(p))) {
      const ua = req.headers.get("user-agent") ?? "";
      if (ua.includes("python-requests") || ua.includes("axios")) {
        return { score: 25, tags: ["scraper:sensitive"] };
      }
    }
    return { score: 0, tags: [] };
  },
};

Event Hooks

export const POST = withHippocrates(handler, schema, redis, {
  hooks: {
    onViolation: (event) => {
      console.log(`${event.ip} - ${event.violations}`);
    },
    onPass: (event) => {
      metrics.recordPass(event.ip, event.score);
    },
    onHoneypot: (event) => {
      alertService.notify(`Honeypot served to ${event.ip}`);
    },
  },
});

Stats Tracking

In-memory counters accessible via ThreatScoreEngine.getStats(). Pass a custom StatsTracker for external persistence:

import { type StatsTracker } from "hippocrates-middleware";

const tracker: StatsTracker = {
  increment(counter) {
    console.log(`Event: ${counter}`);
  },
  getStats() {
    return { totalRequests: 0, blockedByPreflight: 0, /* ... */ };
  },
  reset() {},
};

Warning: In serverless environments (Vercel Edge, AWS Lambda), each cold start creates a fresh ThreatScoreEngine instance -- stats reset on every invocation. Use a custom StatsTracker that persists to an external store for production monitoring.

Available counters: totalRequests, blockedByPreflight, blockedByTiming, blockedByVelocity, blockedByObfuscation, blockedBySchema, passedToHandler, honeypotServed, redisErrors.

Redis-backed StatsTracker example:

import { type StatsTracker } from "hippocrates-middleware";
import { Redis } from "@upstash/redis";

function createRedisStatsTracker(redis: Redis): StatsTracker {
  const key = "hc:stats";

  return {
    increment(counter) {
      redis.hincrby(key, counter, 1).catch(() => {});
    },
    async getStats() {
      const data = await redis.hgetall<Record<string, string>>(key);
      if (!data) {
        return {
          totalRequests: 0, blockedByPreflight: 0, blockedByTiming: 0,
          blockedByVelocity: 0, blockedByObfuscation: 0, blockedBySchema: 0,
          passedToHandler: 0, honeypotServed: 0, redisErrors: 0,
        };
      }
      return Object.fromEntries(
        Object.entries(data).map(([k, v]) => [k, Number(v)]),
      ) as SecurityStats;
    },
    reset() {
      redis.del(key).catch(() => {});
    },
  };
}

Pass it to withHippocrates to persist stats across cold starts:

export const POST = withHippocrates(handler, schema, redis, {
  statsTracker: createRedisStatsTracker(redis),
});

ML Engine (Python Sidecar)

Optional ML-based detection: prompt injection, advanced obfuscation, content risk scoring (SQLi, XSS, path traversal, command injection). Runs in a Python FastAPI sidecar.

docker compose up -d
import { mlEnginePlugin } from "hippocrates-middleware";

export const POST = withHippocrates(handler, schema, redis, {
  plugins: [mlEnginePlugin({
    baseUrl: "http://ml-engine:8000",
    timeoutMs: 3000,
    minScoreThreshold: 10,
  })],
});

If the ML engine is unreachable, it returns score 0 with a ml-engine-unreachable tag. The plugin has its own circuit breaker: 3 consecutive failures trip a 30s cooldown.

ML engine config options:

Option Default Description
baseUrl http://localhost:8000 ML engine endpoint
timeoutMs 3000 Request timeout
minScoreThreshold 10 Minimum ML score to contribute
maxRetries 1 Retries before degrading
circuitBreakerCooldownMs 30000 Cooldown after max failures
maxConsecutiveFailures 3 Failures before circuit trips

License

MIT (c) achmdfzn

About

Next.js security middleware with Redis-backed threat scoring, honeypot decoy routing, and Python ML engine for advanced detection.

Topics

Resources

License

Stars

5 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors