Strapi API Security — CORS, Rate Limiting, Query Complexity, and Sanitization
In this tutorial, you will learn how to secure your Strapi API by configuring CORS for frontend access, implementing Rate Limiting to prevent abuse, analyzing query complexity to block expensive requests, and sanitizing inputs to prevent injection attacks.
What You'll Learn
- How to configure CORS to allow specific frontend origins
- How to set up rate limiting to prevent API abuse
- How to analyze and limit Graphql query complexity
- How input sanitization prevents injection attacks
- How to secure the admin panel against unauthorized access
- Security best practices for Strapi API deployment
Why It Matters
An unsecured API is an invitation to data breaches, service abuse, and financial loss. Strapi APIs often serve public content, but even public APIs need protection against scraping, DDoS attacks, and injection. Proper security configuration keeps your data safe, your server responsive, and your costs under control.
Real-World Use
A content API serves 100,000 daily requests from a React frontend. Without rate limiting, a malicious script can send 1 million requests per minute, causing server overload and a $5,000 cloud bill. Without CORS, an attacker's website can steal data by making requests from the user's browser. Without query complexity limits, a deeply nested GraphQL query can crash the database server.
Learning Path
flowchart LR A["API Customization"] --> B["API Security
-- You are here"]:::current B --> C["Users & Roles"] C --> D["Permissions"] D --> E["Authentication"] classDef current fill:#4945ff,color:#fff,stroke-width:2px
CORS Configuration
CORS (Cross-Origin Resource Sharing) controls which domains can access your API from browser-based applications.
// config/middlewares.js
module.exports = [
"strapi::logger",
"strapi::errors",
{
name: "strapi::security",
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
"connect-src": ["'self'", "https:"],
"img-src": ["'self'", "data:", "blob:", "https://your-frontend.com"],
"media-src": ["'self'", "data:", "blob:", "https://your-frontend.com"],
upgradeInsecureRequests: null,
},
},
},
},
{
name: "strapi::cors",
config: {
enabled: true,
origin: [
"http://localhost:3000", // Local React dev
"https://your-frontend.com", // Production frontend
"https://www.your-frontend.com",
],
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
headers: ["Content-Type", "Authorization", "Origin", "Accept"],
keepHeaderOnError: true,
},
},
// ... other middlewares
];
The origin setting is the most important. In development, include your local frontend URL. In production, list your actual frontend domains. Never use origin: "*" in production — it allows any website to access your API.
Rate Limiting
Rate limiting prevents a single client from making too many requests in a short time.
// config/plugins.js — Rate limiting configuration
module.exports = {
"users-permissions": {
config: {
ratelimit: {
interval: 60 * 1000, // 1 minute window
max: 100, // 100 requests per minute
},
},
},
};
For more granular rate limiting, use the koa-rate-limit middleware:
npm install koa-rate-limit
// config/middlewares.js
module.exports = [
// ... existing middlewares
{
name: "global::rate-limiter",
config: {
interval: { min: 1 }, // 1 minute
max: 60, // 60 requests per minute
message: "Too many requests, please try again later.",
statusCode: 429,
},
},
];
Rate limiting is essential for public APIs. Without it, a single aggressive client can consume all your server resources.
GraphQL Query Complexity
GraphQL queries can be dangerously expensive. A simple-looking query can request thousands of database records through nested relations.
// config/plugins.js — GraphQL security
module.exports = {
graphql: {
config: {
endpoint: "/graphql",
shadowCRUD: true,
apolloServer: {
introspection: false, // Disable in production
},
maxDepth: 5, // Maximum query depth
maxComplexity: 1000, // Maximum query complexity score
},
},
};
Query complexity assigns a cost to each field. A query that exceeds the maxComplexity limit is rejected with an error. This prevents attackers from crafting expensive queries that exhaust database resources.
// Example: This query has high complexity
// It requests articles, their authors, the author's articles, those articles' tags...
// A limit of 1000 would likely block this
query {
articles {
data {
attributes {
title
author {
data {
attributes {
articles {
data {
attributes { title tags { data { attributes { name } } } }
}
}
}
}
}
}
}
}
}
Input Sanitization
Strapi automatically sanitizes inputs to prevent common injection attacks. However, you should be aware of what it sanitizes and add additional protection where needed.
// Strapi's built-in sanitization:
// - Strips HTML tags from string fields (configurable)
// - Validates field types against schema definitions
// - Escapes database queries via Knex.js parameterized queries
// Custom sanitization middleware
// src/middlewares/sanitize-input.js
module.exports = (config, { strapi }) => {
return async (ctx, next) => {
if (ctx.request.body?.data) {
// Strip script tags from all string fields
const sanitize = (obj) => {
for (const key in obj) {
if (typeof obj[key] === "string") {
obj[key] = obj[key].replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
} else if (typeof obj[key] === "object") {
sanitize(obj[key]);
}
}
};
sanitize(ctx.request.body.data);
}
await next();
};
};
Strapi's built-in sanitization is sufficient for most use cases. Add custom sanitization if your application accepts HTML input and needs to restrict allowed tags.
Admin Panel Security
The admin panel is a prime target for attackers. Protect it with these configurations:
// config/admin.js
module.exports = ({ env }) => ({
auth: {
secret: env("ADMIN_JWT_SECRET"),
},
apiToken: {
salt: env("API_TOKEN_SALT"),
},
transfer: {
token: {
salt: env("TRANSFER_TOKEN_SALT"),
},
},
// Restrict admin access by IP
admin: {
watchIgnoreFiles: [],
autoOpen: false,
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 1337),
serveAdminPanel: true,
},
});
Best practices for admin security:
- Change the default admin URL from
/adminto a custom path using a reverse proxy - Enable HTTPS for all admin traffic
- Use strong admin passwords and enforce 2FA
- Restrict admin access by IP address using firewall rules
- Disable admin registration after initial setup
Security Headers
Add security headers to protect against common web attacks:
// config/middlewares.js
{
name: "strapi::security",
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "'unsafe-inline'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "blob:", "https:"],
"connect-src": ["'self'", "https:"],
"frame-ancestors": ["'none'"],
"form-action": ["'self'"],
},
},
},
}
These headers prevent XSS (cross-site scripting), clickjacking, and content injection attacks.
Common Mistakes
Using wildcard CORS in production. Setting
origin: "*"allows any website to make API requests from browsers. This exposes your API to cross-site request forgery and data theft.Not rate limiting public endpoints. Public API endpoints without rate limits are vulnerable to abuse. Implement rate limits on all public endpoints, especially authentication and content creation.
Exposing GraphQL introspection in production. Introspection exposes your entire schema, including internal types and relationships. Attackers use this to find weak points. Disable introspection in production.
Leaving the admin panel exposed. The default
/adminroute is well-known. Restrict access by IP, use a custom URL path, or put it behind a VPN for production.Trusting user input without validation. Even with Strapi's built-in sanitization, validate input data in your controllers and services. Never pass raw user input to database queries or file system operations.
Practice Questions
What is CORS and why must you configure it for production? Answer: CORS controls which domains can access your API from browsers. Without proper CORS configuration, any website can make requests to your API from a user's browser, potentially stealing data.
How does rate limiting protect your Strapi API? Answer: Rate limiting restricts the number of requests from a single client within a time window. It prevents brute force attacks, API abuse, and excessive resource consumption.
Why should you limit GraphQL query complexity and depth? Answer: Malicious GraphQL queries can request deeply nested data that generates thousands of database queries. Complexity and depth limits reject expensive queries before they execute.
Challenge: Perform a security audit of your Strapi project: (1) Check CORS configuration and verify only your frontend domains are allowed, (2) Enable rate limiting and test that it blocks excessive requests, (3) Disable GraphQL introspection and verify the playground is inaccessible, (4) Test input sanitization by sending malicious payloads, (5) Review admin panel security and implement IP restriction. Document all findings and fixes.
FAQ
Mini Project
Your task: Implement a complete security configuration for a production Strapi API.
- Configure CORS to allow only your frontend domain (use
http://localhost:3000for testing). - Implement rate limiting with koa-rate-limit: 60 requests per minute per IP.
- If using GraphQL, set max depth to 5 and disable introspection.
- Create a custom security middleware that adds security headers and logs suspicious requests.
- Configure environment-specific security settings using
.envvariables. - Write a test script that attempts to exceed rate limits, send malicious payloads, and access the API from disallowed origins. Verify all protections work.
What's Next
Now that your API is secured, proceed to Users & Roles to learn about Strapi's user management system, including authenticated and public roles. After that, set up fine-grained Permissions for your content types.
Related lessons:
- REST API Security — General API security practices
- GraphQL Security — GraphQL-specific security
- Node.js Security — Securing Node.js applications
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro