Preventing SSRF in Strapi 5: Configuration and Defensive Coding Guide
Learn how to stop Server‑Side Request Forgery in Strapi 5 by using proxy settings, URL whitelisting, security headers, rate limiting, and audit‑log monitoring.
By the Revisionary team · · 3 min read
Strapi 5 can be vulnerable to Server‑Side Request Forgery (SSRF) when developers let the CMS fetch arbitrary URLs—e.g., in webhooks or custom strapi.fetch calls. The simplest protection is to block any request that does not point to an approved host and to enforce this both in configuration and in code. The following guide shows how to use Strapi’s built‑in proxy settings, security middleware, rate limiting, and audit‑log features, plus defensive coding patterns, to eliminate SSRF risk.
1. Understand the SSRF surface in Strapi
Strapi uses an internal helper strapi.fetch for outbound HTTP calls. These calls are used by:
- Webhook delivery (admin‑defined URLs)
- License and telemetry checks
- Custom plugins that request external services
If an attacker can supply a URL that resolves to an internal address—localhost, 127.0.0.1, or any private IP—they can reach internal services, metadata endpoints, or the Docker socket. The Strapi security advisory GHSA‑v8wj‑f5c7‑pvxf (pre‑4.25.2) highlighted this issue, and the same code path exists in Strapi 5.
2. Configure a strict proxy for outbound requests
Strapi’s server configuration (/config/server.js) includes a proxy object that lets you supply a custom HTTP/HTTPS agent for all strapi.fetch calls.
// /config/server.js
module.exports = ({ env }) => ({
// ...other server settings
proxy: {
// Enable proxy handling – keep true unless you deliberately disable it
koa: env.bool('IS_PROXIED', true),
// Optional: set the header that contains the original client IP
ipHeader: env('PROXY_IP_HEADER', 'X-Forwarded-For'),
maxIpsCount: env.int('MAX_IPS_COUNT', 1),
// --- SSRF protection starts here ---
fetch: {
// Use a whitelist‑checking agent (see code below)
request: (url, options) => {
const { URL } = require('url');
const allowedHosts = new Set([
'api.trustedpartner.com',
'hooks.mycompany.com',
]);
const u = new URL(url);
if (u.protocol !== 'https:') {
throw new Error('Only HTTPS destinations are allowed');
}
if (!allowedHosts.has(u.host)) {
throw new Error(`Host ${u.host} is not whitelisted`);
}
// Forward to the real fetch implementation
return fetch(url, options);
},
},
// Optional: route all plain HTTP through a corporate proxy
http: env('HTTP_PROXY_URL'),
https: env('HTTPS_PROXY_URL'),
},
});The fetch property overrides the default request handler. It validates the URL before the request is sent, rejecting any non‑HTTPS scheme and any host not present in the whitelist. This single point of control protects all built‑in and custom uses of strapi.fetch.
2.1 Using a third‑party keep‑alive agent (optional)
If you need connection reuse, wrap the whitelist logic around a keep‑alive agent:
const { HttpsAgent } = require('agentkeepalive');
const keepAliveAgent = new HttpsAgent({ keepAlive: true });
fetch: {
request: (url, options) => {
// URL validation as above …
return fetch(url, { ...options, httpsAgent: keepAliveAgent });
},
},3. Harden the webhook UI
Webhooks are defined by administrators, but a compromised admin account could inject a malicious URL. Enforce validation at the UI level as well:
// Example validation in a custom webhook plugin
module.exports = ({ strapi }) => ({
async createWebhook(ctx) {
const { url } = ctx.request.body;
const { URL } = require('url');
const allowedHosts = ['hooks.mycompany.com'];
const u = new URL(url);
if (u.protocol !== 'https:' || !allowedHosts.includes(u.host)) {
return ctx.badRequest('Invalid webhook URL');
}
// Proceed with creation
return strapi.plugin('webhooks').service('webhook').create(ctx.request.body);
},
});4. Add Content‑Security‑Policy (CSP) headers
Even though CSP mainly protects browsers, configuring a strict connect-src directive reduces the chance that malicious front‑end code can trigger SSRF‑like requests from the client side.
// /config/middlewares.js
module.exports = [
'strapi::errors',
'strapi::security',
// other middlewares …
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
"connect-src": ["'self'", "https:"],
// Keep other defaults (img-src, script-src, etc.)
},
},
},
},
];The useDefaults: true flag ensures Strapi’s baseline CSP ('self') is kept, while the custom connect-src only allows HTTPS origins.
5. Rate‑limit endpoints that accept URLs
If you expose an API endpoint that lets users submit a URL (e.g., a “preview fetch” endpoint), add rate limiting to prevent brute‑force scanning of internal addresses.
// /config/plugins.js
module.exports = ({ env }) => ({
'users-permissions': {
ratelimit: {
enabled: true,
interval: 60000, // 1 minute
max: 10, // 10 requests per minute per IP
},
},
});For more granular control you can add a global Koa rate‑limit middleware:
// /config/middlewares.js (after existing middlewares)
const RateLimit = require('koa2-ratelimit').RateLimit;
module.exports = [
// …previous middlewares
{
name: 'global::rateLimit',
config: {
interval: { min: 5 }, // 5‑minute window
max: 100, // 100 requests per window per IP
headers: true,
},
},
];6. Monitor with audit logs
Strapi’s audit‑log feature records every webhook creation, token generation, and admin authentication event. Enable it and keep logs for at least 30 days to detect suspicious SSRF attempts.
// /config/admin.js
module.exports = ({ env }) => ({
auditLogs: {
enabled: true,
retentionDays: 30,
// Optional: limit export size to avoid DoS on log extraction
exportMaxRows: 500000,
},
});When a blocked request occurs (the whitelist throws), you can log a custom event in the same lifecycle hook:
strapi.log.warn('Blocked SSRF attempt', { url, user: ctx.state.user?.id });
await strapi.services['admin::audit-log'].create({
action: 'ssrf.blocked',
user: ctx.state.user?.id,
payload: { url },
});These entries appear in the audit‑log UI and can be exported for compliance audits.
7. Rotate secrets regularly
Even though SSRF is a network‑level issue, keeping JWT_SECRET, APP_KEYS, and any API keys used for outbound calls fresh reduces the impact of a compromised server.
- Store secrets in environment variables, not in code.
- Rotate them at least quarterly.
- After rotation, invalidate existing sessions via the admin UI or by clearing the session store.
8. Supply‑chain hygiene
SSRF exploits often combine a vulnerable dependency with an open outbound request. Keep your Strapi installation up to date and scan for known vulnerabilities:
- Enable Dependabot or GitHub security alerts on the repository.
- Run
npm audit --audit-level=highin CI. - Subscribe to the Strapi security advisory feed (e.g., GHSA‑v8wj‑f5c7‑pvxf).
9. Quick checklist
| ✅ | Action |
|---|---|
| 1 | Define a static whitelist of allowed hosts in /config/server.js. |
| 2 | Enforce HTTPS scheme for all outbound URLs. |
| 3 | Add URL validation in any custom endpoint that accepts user‑supplied URLs. |
| 4 | Configure strapi::security CSP with a restrictive connect-src. |
| 5 | Enable rate limiting on URL‑accepting routes. |
| 6 | Turn on audit logs and monitor for ssrf.blocked events. |
| 7 | Rotate JWT and APP_KEYS regularly. |
| 8 | Keep Strapi and all npm dependencies up to date. |
10. When to involve Revisionary
If you already use the Revisionary plugin for content versioning, you can leverage its audit‑log integration to retain a permanent record of every blocked SSRF attempt. The plugin’s audit‑log export makes it easy to provide evidence of defensive actions during GDPR or internal compliance reviews.
By applying the built‑in proxy configuration, CSP headers, rate limiting, and audit‑log monitoring, developers can eliminate the SSRF attack surface in Strapi 5 without adding external libraries. The approach is declarative, works for all built‑in and custom fetch calls, and aligns with OWASP’s SSRF prevention recommendations.
Further reading
- OWASP Server‑Side Request Forgery Prevention Cheat Sheet
- Strapi server configuration documentation
- Strapi security middleware guide
- Strapi audit‑log feature page
- Strapi blog post on rate‑limiting best practices
Sources
The Revisionary team
Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.
This article was written with the help of AI.