# How Ghost Handles CORS for Its API Endpoints: Express Middleware Deep Dive

> Ghost manages CORS for API endpoints using express cors middleware and a custom delegate function. Discover how Ghost validates requests against a dynamic allow-list of trusted origins.

- Repository: [Ghost/Ghost](https://github.com/TryGhost/Ghost)
- Tags: deep-dive
- Published: 2026-05-18

---

**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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/cors.js)) to determine CORS eligibility:

1. **Header inspection**: The middleware reads the `Origin` header from the request
2. **Null origin handling**: If the origin is missing or the string `"null"`, Ghost treats the request as non-CORS and disables CORS headers
3. **Hostname matching**: The delegate parses the hostname from the origin URL and checks it against the cached allow-list
4. **Boolean decision**: If the hostname matches, the delegate returns `origin: true` (enabling CORS); otherwise, it returns `origin: 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`](https://github.com/TryGhost/Ghost/blob/main/cors.js) module exports two critical middleware components (lines 98-100):

- **`corsMiddleware`**: The primary handler wrapping the `cors` package with the custom delegate
- **`corsCaching`**: A specialized helper that adds the `Vary: Origin` header to pre-flight `OPTIONS` responses

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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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:

```javascript
// 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`](https://github.com/TryGhost/Ghost/blob/main/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:

```javascript
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:

```javascript
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`](https://github.com/TryGhost/Ghost/blob/main/config.production.json)):

```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()` and `getUrls()` in [`cors.js`](https://github.com/TryGhost/Ghost/blob/main/cors.js), including localhost, local IPs, and configured URLs.
- **Request validation**: The `corsOptionsDelegate` function performs strict hostname matching against this allow-list for every request, returning boolean flags to the `cors` package.
- **Middleware stack**: Two components—`corsMiddleware` and `corsCaching`—handle standard CORS headers and `Vary: Origin` caching headers respectively.
- **Universal coverage**: The middleware mounts in [`api/middleware/index.js`](https://github.com/TryGhost/Ghost/blob/main/api/middleware/index.js) and 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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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.