express-sus
v0.0.2
Published
**express-sus** is a flexible Express.js middleware for detecting and preventing suspicious behavior through a dynamic scoring system. Track user actions, assign scores based on suspicious activities, and automatically block requests when thresholds are e
Maintainers
Readme
express-sus (SusFactor)
express-sus is a flexible Express.js middleware for detecting and preventing suspicious behavior through a dynamic scoring system. Track user actions, assign scores based on suspicious activities, and automatically block requests when thresholds are exceeded.
What is express-sus?
express-sus helps you protect your Express.js applications by:
- Tracking suspicious behavior - Monitor and score potentially harmful actions
- Flexible rate limiting - Block requests based on customizable score thresholds
- Multiple boundaries - Set different limits for different parts of your application
- Persistent storage - Choose between in-memory or Redis-based caching
- Automatic score decay - Reduce scores over time to allow legitimate users to recover
- Custom handlers - Define your own logic for blocked requests
Features
- Score-based blocking - Assign scores to suspicious activities and block when thresholds are exceeded
- Multiple zones - Create isolated scoring zones for different parts of your application
- Flexible caching - Built-in support for in-memory and Redis cache providers
- Automatic score reduction - Configure periodic score decay to forgive past behavior
- Boundaries - Set multiple score thresholds throughout your application
- Customizable - Override default behaviors with custom handlers and ID generators
- Debug headers - Optionally expose scores and request IDs in response headers
Installation
npm install express-susFor Redis support, also install:
npm install @redis/clientQuick Start
Basic Usage (In-Memory Cache)
const express = require('express');
const SusFactor = require('express-sus');
const app = express();
// Initialize SusFactor with default settings
const sus = new SusFactor({
maximumScore: 100,
addScoreToHeaders: true // For debugging
});
// Add the middleware to your app
app.use(sus.getMiddleware());
// Create a boundary to block high-score requests
app.use(sus.getBoundary(100).getHandler());
app.get('/', (req, res) => {
// Detect suspicious behavior and add to score
if (req.query.username?.includes('<script>')) {
req.sus.main.addToScore(50); // Potential XSS attempt
}
res.send('Hello World!');
});
app.listen(3000);Using Redis Cache
const express = require('express');
const SusFactor = require('express-sus');
const RedisCacheProvider = require('express-sus/lib/RedisCacheProvider');
const app = express();
// Create Redis cache provider
const redisCache = new RedisCacheProvider({
ttl: 3600 // Scores expire after 1 hour
});
// Initialize SusFactor with Redis
const sus = new SusFactor({
cacheProvider: redisCache,
maximumScore: 100,
scoreDecutionThreshold: 60000, // Reduce scores every minute
scoreDecutionAmount: 10 // Reduce by 10 points
});
app.use(sus.getMiddleware());
app.use(sus.getBoundary(100).getHandler());
app.get('/', (req, res) => {
res.send('Hello World!');
});
async function start() {
// Connect to Redis
await redisCache.connect({ url: 'redis://localhost:6379' });
app.listen(3000, () => {
console.log('Server running on port 3000');
});
}
start();Configuration
SusFactor Options
const sus = new SusFactor({
// Add current score to response headers (default: false)
addScoreToHeaders: true,
// Add request ID to response headers (default: false)
addRequestIdToHeaders: true,
// Maximum score before blocking (default: 100)
maximumScore: 100,
// Zone name for this instance (default: "main")
zone: "api",
// Interval in milliseconds for automatic score reduction (default: null)
scoreDecutionThreshold: 5000,
// Amount to reduce scores by at each interval (default: null)
scoreDecutionAmount: 50,
// Cache provider instance (default: InMemoryCacheProvider)
cacheProvider: customCacheProvider,
// Function to generate request IDs (default: IP-based)
requestIdGenerator: (req) => req.headers['x-custom-id'],
// Custom handler for blocked requests (default: 429 response)
onBlockedHandler: (req, res) => {
res.status(403).json({ error: 'Access Denied' });
}
});Cache Provider Options
InMemoryCacheProvider (Default)
const InMemoryCacheProvider = require('express-sus/lib/InMemoryCacheProvider');
const cache = new InMemoryCacheProvider({
zone: "main" // Zone name (default: "main")
});RedisCacheProvider
const RedisCacheProvider = require('express-sus/lib/RedisCacheProvider');
const cache = new RedisCacheProvider({
zone: "main", // Zone name (default: "main")
prefix: "sus-factor", // Key prefix (default: "sus-factor")
ttl: 3600 // Time-to-live in seconds (default: 3600, set to false to disable)
});
// Connect to Redis
await cache.connect({
url: 'redis://localhost:6379'
});Working with Scores
After applying the middleware, each request gets a req.sus[zone] object with methods to manage scores:
app.get('/api/data', async (req, res) => {
// Get current score
const score = await req.sus.main.getScore();
// Set score to a specific value
await req.sus.main.setScore(50);
// Add to the score
await req.sus.main.addToScore(25);
// Reset score to zero
await req.sus.main.resetScore();
// Check if request should be blocked
const blocked = await req.sus.main.isBlocked();
const blockedCustom = await req.sus.main.isBlocked(75); // Custom threshold
// Get the request ID
const requestId = req.sus.main.id;
res.json({ score, blocked });
});Boundaries
Boundaries allow you to set different score thresholds at various points in your application:
const sus = new SusFactor({ maximumScore: 100 });
app.use(sus.getMiddleware());
// Global boundary - blocks at score 100
app.use(sus.getBoundary().getHandler());
// Protected routes with stricter boundary
app.use('/admin', sus.getBoundary(50).getHandler());
app.get('/admin/dashboard', (req, res) => {
res.send('Admin Dashboard');
});
// Public routes with lenient boundary
app.use('/public', sus.getBoundary(200).getHandler());
app.get('/public/info', (req, res) => {
res.send('Public Info');
});Advanced Usage
Multiple Zones
Create isolated scoring zones for different parts of your application:
const apiSus = new SusFactor({
zone: "api",
maximumScore: 100
});
const authSus = new SusFactor({
zone: "auth",
maximumScore: 10 // Stricter for auth
});
app.use(apiSus.getMiddleware());
app.use(authSus.getMiddleware());
app.post('/api/data', async (req, res) => {
// Track both zones independently
await req.sus.api.addToScore(5);
await req.sus.auth.addToScore(1);
res.json({ message: 'Success' });
});Custom Request ID Generator
By default, request IDs are generated from IP addresses. You can customize this:
const sus = new SusFactor({
requestIdGenerator: (req) => {
// Use user ID if authenticated, otherwise IP
return req.user?.id || req.ip || 'anonymous';
}
});Custom Blocked Handler
Define what happens when a request is blocked:
const sus = new SusFactor({
onBlockedHandler: (req, res, next) => {
console.log(`Blocked request from ${req.ip}`);
res.status(403).json({
error: 'Access Denied',
message: 'Too many suspicious activities detected',
retryAfter: 3600
});
}
});Score Patterns for Common Threats
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// SQL injection attempt
if (/['";]|(\bOR\b)|(\bAND\b)/i.test(username)) {
await req.sus.main.addToScore(100); // Immediate block
}
// Invalid credentials
const user = await authenticate(username, password);
if (!user) {
await req.sus.main.addToScore(10); // Progressive blocking
}
res.json({ success: !!user });
});
app.get('/api/products', async (req, res) => {
// XSS attempt in query params
if (/<script|javascript:|onerror=/i.test(req.query.search)) {
await req.sus.main.addToScore(75);
}
// Path traversal attempt
if (/\.\.|\/etc\/|\/proc\//i.test(req.query.file)) {
await req.sus.main.addToScore(90);
}
res.json({ products: [] });
});API Reference
SusFactor Class
Constructor
new SusFactor(options)Methods
getMiddleware()- Returns Express middleware functiongetBoundary(score)- Creates a new boundary with optional custom scoregetZone()- Returns the zone namedecreaseAllScoresBy(amount)- Manually decrease all scoresstartScoreDecutionInterval()- Start automatic score reductionstopScoreDecutionInterval()- Stop automatic score reduction
Request Object Extensions
After middleware is applied, req.sus[zone] contains:
id- The request ID (string)getScore()- Get current score (async)setScore(score)- Set score to specific value (async)addToScore(delta)- Add/subtract from score (async)resetScore()- Reset score to zero (async)isBlocked(threshold?)- Check if score exceeds threshold (async)
Cache Providers
Both cache providers implement:
getScore(key)- Get score for a keysetScore(key, score)- Set score for a keyaddToScore(key, delta)- Add to scoreresetScore(key)- Reset score to zeroaddToAllScores(delta)- Add to all scores
Testing
Run the test suite:
npm testRun linting:
npm run lintExample Application
See example.js for a complete working example with Redis integration.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
ISC
Links
Use Cases
- Rate limiting - Prevent API abuse
- Brute force protection - Slow down login attempts
- Bot detection - Score and block automated traffic
- Security monitoring - Track suspicious patterns (XSS, SQL injection attempts)
- Progressive penalties - Increasingly strict responses to repeat offenders
- DDoS mitigation - Block high-volume attackers
Best Practices
- Start with conservative scores - Don't block too aggressively at first
- Monitor false positives - Use
addScoreToHeadersduring development - Implement score decay - Allow legitimate users to recover from mistakes
- Use Redis for production - In-memory cache doesn't persist across restarts
- Combine with other security - express-sus complements but doesn't replace authentication
- Test your thresholds - Different applications need different sensitivity levels
