How Ghost Handles CORS for Its API Endpoints: Express Middleware Deep Dive
Ghost validates every cross-origin request against a dynamically generated allow-list of trusted hostnames, using the express cors package with a custom delegate function to selectively enable CORS headers only for recognized origins.
Ghost implements Cross-Origin Resource Sharing (CORS) for its public Content API and private Admin API through a centralized middleware system located in the core server package. The implementation combines the standard cors npm package with custom origin validation logic to ensure that only legitimate requests from configured domains, localhost environments, and the admin UI receive appropriate Access-Control-Allow-Origin headers.
The Three-Stage CORS Pipeline
The CORS implementation lives in ghost/core/core/server/web/api/middleware/cors.js and operates through a strict three-stage pipeline that runs at server startup and during every incoming request.
Stage 1: Building the Allow-List at Startup
When Ghost initializes, it constructs a cached allow-list of permissible origins by aggregating three distinct sources:
- Local development addresses: All localhost variants and local IPv4 addresses discovered via
getIPs() - Configured site URLs: The primary blog URL and admin URL from Ghost's configuration
- Internal references: Any additional domains explicitly set in the config files
This construction happens once during boot inside getIPs() and getUrls() (lines 15-50 of cors.js), ensuring the allow-list remains static and performant during runtime.
Stage 2: Dynamic Origin Validation Per Request
For every incoming API request, Ghost executes the corsOptionsDelegate function (line 68 of cors.js) to determine CORS eligibility:
- Header inspection: The middleware reads the
Originheader from the request - Null origin handling: If the origin is missing or the string
"null", Ghost treats the request as non-CORS and disables CORS headers - Hostname matching: The delegate parses the hostname from the origin URL and checks it against the cached allow-list
- Boolean decision: If the hostname matches, the delegate returns
origin: true(enabling CORS); otherwise, it returnsorigin: false
This strict hostname validation prevents arbitrary third-party websites from making authenticated requests to your Ghost instance while permitting legitimate headless front-ends and the Ghost admin application.
Stage 3: Middleware Application and Caching Headers
The cors.js module exports two critical middleware components (lines 98-100):
corsMiddleware: The primary handler wrapping thecorspackage with the custom delegatecorsCaching: A specialized helper that adds theVary: Originheader to pre-flightOPTIONSresponses
The Vary: Origin header is crucial for proper CDN and browser caching behavior, ensuring that caches distinguish between responses served to different origins.
Mounting CORS in the API Router
The middleware integration occurs in ghost/core/core/server/web/api/middleware/index.js, where both corsCaching and corsMiddleware are registered to the main API router stack. This registration ensures that every endpoint under /ghost/api/ automatically receives CORS handling.
Individual route files apply additional configuration as needed. For example, in ghost/core/core/server/web/api/endpoints/content/routes.js (line 14), the Content API applies a specific maxAge configuration to cache pre-flight responses:
// From content/routes.js
router.use(cors({maxAge: corsMaxAge}));
The Members API implements parallel logic in ghost/core/core/server/web/members/middleware/cors.js, ensuring consistent security across all public-facing JSON endpoints.
Practical Cross-Origin Request Examples
Fetching Public Content from a Different Origin
When accessing the Content API from a headless front-end, no special configuration is required because Ghost automatically adds CORS headers for recognized origins:
fetch('https://my-ghost-site.com/ghost/api/content/posts/', {
method: 'GET',
credentials: 'omit', // Public API requires no authentication cookies
headers: {
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => console.log(data.posts));
Ghost responds with appropriate Access-Control-Allow-Origin headers matching your front-end's domain, provided that domain resolves to a hostname in the allow-list.
Accessing the Admin API with Credentials
Authenticated requests to the private Admin API require credential inclusion, triggering a CORS pre-flight request:
fetch('https://my-ghost-site.com/ghost/api/admin/posts/', {
method: 'GET',
credentials: 'include', // Required for cookie-based auth
headers: {
'Authorization': `Ghost ${ADMIN_API_KEY}`,
'Accept': 'application/json'
}
})
.then(r => r.json())
.then(posts => console.log(posts));
Because credentials: 'include' is set, the browser first sends an OPTIONS pre-flight request. Ghost's corsCaching middleware responds with the Vary: Origin header, while corsMiddleware validates the origin against the allow-list before permitting the actual GET request.
Configuring Custom Origins for Staging Environments
To add custom domains to the CORS allow-list, modify your Ghost configuration file (e.g., config.production.json):
{
"url": "https://production-site.com",
"admin": {
"url": "https://admin.production-site.com"
},
"cors": {
"maxAge": 86400
}
}
Ghost's getUrls() function automatically ingests these values during the startup allow-list construction, immediately enabling CORS for requests originating from https://admin.production-site.com.
Summary
- Allow-list construction: Ghost generates a cached list of trusted origins at startup via
getIPs()andgetUrls()incors.js, including localhost, local IPs, and configured URLs. - Request validation: The
corsOptionsDelegatefunction performs strict hostname matching against this allow-list for every request, returning boolean flags to thecorspackage. - Middleware stack: Two components—
corsMiddlewareandcorsCaching—handle standard CORS headers andVary: Origincaching headers respectively. - Universal coverage: The middleware mounts in
api/middleware/index.jsand applies to all API endpoints, including Content, Admin, and Members routes. - Test coverage: The implementation is validated by unit tests in
ghost/core/test/unit/server/web/api/middleware/cors.test.js.
Frequently Asked Questions
What origins does Ghost allow by default for CORS?
Ghost automatically permits CORS requests from localhost (all variants), every local IPv4 address on the server, and any domains specified in the url and admin.url configuration values. This covers standard development environments and the officially configured site domains without requiring manual CORS configuration.
How does Ghost handle pre-flight OPTIONS requests?
Ghost intercepts OPTIONS requests using the corsCaching middleware exported from api/middleware/cors.js, which explicitly sets the Vary: Origin header. This ensures that browser caches and CDNs store separate responses for different origins, preventing cache poisoning while allowing legitimate pre-flight caching.
Can I add custom domains to the Ghost CORS allow-list?
Yes, by adding the domain to your Ghost configuration's url or admin.url fields. Ghost rebuilds the allow-list from these configuration values at startup, so a simple restart after updating config.production.json or environment variables will include new domains in the CORS validation logic.
Where is the CORS configuration tested in the Ghost codebase?
The core CORS logic is extensively unit-tested in ghost/core/test/unit/server/web/api/middleware/cors.test.js. These tests verify the allow-list generation functions, the hostname matching logic in corsOptionsDelegate, and the correct header injection behavior for both allowed and disallowed origins.
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 →