How Ghost Implements Spam Prevention in Its API: Express-Brute and Knex Store Architecture
Ghost prevents API spam using a centralized rate-limiting layer built on express-brute and brute-knex that stores request counters in a Knex-backed database table and returns HTTP 429 errors with customizable retry delays.
Ghost protects its public authentication, content, and management endpoints using a sophisticated rate-limiting system implemented in ghost/core/core/server/web/shared/middleware/api/spam-prevention.js. The architecture leverages Express-Brute with a Knex-backed store to persist request counters across server restarts, enabling precise throttling for high-risk operations like login attempts and password resets.
Core Architecture: Database-Backed Rate Limiting
The spam prevention system relies on express-brute middleware paired with brute-knex to store rate-limiting state in the database rather than memory. This ensures that request counters survive server restarts and work correctly in multi-instance deployments.
The Knex Store Implementation
At initialization, the system creates a single ExpressBrute instance backed by a Knex store named brute. This store writes to a dedicated brute table in the database, tracking request counts and timestamps per client identifier. By using database persistence instead of in-memory storage, Ghost ensures that rate limits remain consistent across load-balanced environments.
Configuration-Driven Limits
Each limiter reads its constraints from config.get('spam'), with sensible defaults defined directly in the spam-prevention module. The configuration object supports standard Express-Brute parameters including:
freeRetries: Number of allowed attempts before throttling beginsminWaitandmaxWait: Minimum and maximum wait times between retrieslifetime: Duration that request records persist in the database
Endpoint-Specific Rate Limiting Rules
Ghost defines separate ExpressBrute instances for distinct attack vectors, each with tailored limits and key generation strategies.
Global IP-Based Protection
The globalBlock limiter protects against brute-force attacks on any IP-based endpoint, allowing 50 attempts per hour before blocking for one hour. For sensitive operations like password reset token generation, the globalReset limiter enforces stricter limits of 5 attempts per hour with a similar one-hour block duration.
User-Specific Authentication Throttling
The userLogin limiter implements a Fibonacci back-off strategy tied to the combination of user identity and IP address. It permits 5 attempts per user-IP pair, with exponentially increasing delays between subsequent requests. This prevents credential stuffing while minimizing impact on legitimate users who might mistype passwords.
Member Authentication and Enumeration Protection
For member sign-ins, the membersAuth limiter restricts attempts to 5 per email address per hour. The membersAuthEnumeration limiter uses similar constraints but resets its counter on successful authentication, preventing attackers from testing email validity without triggering rate limits on active accounts.
One-Time Code and Specialized Limiters
Ghost includes dedicated limiters for otcVerification (one-time code verification) and otcVerificationEnumeration, both allowing 5 attempts per code or IP address. Additional limiters protect specific features: privateBlog (10 attempts per hour for private site access), contentApiKey (configurable per-key limits using an in-memory store), webmentionsBlock (prevents mention spam), and emailPreviewBlock (limits test emails to 10 per hour).
Middleware Integration with Express Routes
Individual rate limiters are exposed through ghost/core/core/server/web/shared/middleware/brute.js, which translates each spam-prevention rule into standard Express middleware. This abstraction allows routes to apply rate limiting without managing Express-Brute configuration directly.
For example, attaching member authentication protection to a route:
// In the members service router
const brute = require('../../../../web/shared/middleware/brute');
router.post('/signin', brute.membersAuth, async (req, res, next) => {
// Normal sign-in logic executes only if rate limit permits
});
The brute middleware factory wraps the underlying ExpressBrute instances with getMiddleware() calls, passing route-specific options like ignoreIP and custom key generation functions.
Error Handling and Client Feedback
When a request exceeds its allocated rate limit, the failCallback defined in spam-prevention.js creates a TooManyRequestsError with HTTP status 429. The error response includes a human-readable message indicating the remaining wait time, constructed using the minWait and maxWait configuration values.
The system also centralizes store error handling, ensuring that database connection failures or Knex errors are logged and transformed into appropriate HTTP errors rather than crashing the request pipeline.
Practical Configuration and Usage Examples
Attaching Limiters to Routes
Routes import the brute middleware and apply specific limiters as middleware functions:
const brute = require('ghost/core/core/server/web/shared/middleware/brute');
// Protect password reset endpoint
router.post('/authentication/passwordreset', brute.globalReset, controller);
Manual Limiter Management
For testing or administrative purposes, limiters expose a reset() method that clears counters from both memory and the database:
const spamPrevention = require('ghost/core/core/server/web/shared/middleware/api/spam-prevention');
// Clear all counters for user login attempts
await spamPrevention.userLogin().reset();
Customizing Rate Limits
Administrators can override default limits via config.production.json or environment variables:
{
"spam": {
"user_login": {
"freeRetries": 5,
"minWait": 60,
"maxWait": 3600,
"lifetime": 86400
},
"global_block": {
"freeRetries": 50,
"minWait": 3600000,
"lifetime": 3600
}
}
}
Changes to the config/spam section take effect without server restart when invoking the exported reset() method on the spam prevention module.
Summary
- Ghost implements API spam prevention using
express-brutemiddleware with abrute-knexstore that persists request data to a database table namedbrute - Rate limits are configurable via
config.get('spam')with defaults defined inspam-prevention.js - Separate limiters protect specific attack vectors including global IP blocks, user logins, member authentication, and one-time code verification
- Exceeded limits return HTTP 429
TooManyRequestsErrorwith human-readable retry timing - The
reset()method allows clearing counters and reloading configuration without server restart
Frequently Asked Questions
What database table does Ghost use for rate limiting storage?
Ghost stores rate-limiting counters in a table named brute, accessed through the brute-knex store adapter. This ensures persistence across server restarts and compatibility with multi-instance deployments, unlike in-memory storage alternatives.
How does Ghost handle requests that exceed rate limits?
When a client exceeds its allocated attempts, the failCallback in spam-prevention.js generates a TooManyRequestsError with HTTP status 429. The response includes a human-readable message indicating how long the client must wait before retrying, calculated from the configured minWait and maxWait values.
Can rate limiting configuration be changed without restarting Ghost?
Yes. The spam prevention module exports a reset() method that clears all in-memory ExpressBrute instances and reloads configuration from config.get('spam'). This allows runtime updates to rate limits by calling the reset method after modifying configuration files.
What is the difference between the globalBlock and userLogin limiters?
The globalBlock limiter tracks attempts by IP address alone (50 attempts per hour), protecting against general brute-force attacks. The userLogin limiter tracks attempts by the combination of user identity and IP address using Fibonacci back-off, specifically preventing credential stuffing while reducing false positives for legitimate users who mistype passwords.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →