Skip to content

Strapi API Security — CORS, Rate Limiting, Query Complexity, and Sanitization

DodaTech Updated 2026-06-28 8 min read

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 /admin to 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

  1. 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.

  2. 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.

  3. 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.

  4. Leaving the admin panel exposed. The default /admin route is well-known. Restrict access by IP, use a custom URL path, or put it behind a VPN for production.

  5. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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

Can I use environment variables for CORS origins?

Yes. Use env('CORS_ORIGIN', '').split(',') to read allowed origins from an environment variable. This is the recommended approach for different environments (dev, staging, production).

What is the default rate limit in Strapi?

Strapi does not have a default rate limit on the REST API. The Users & Permissions plugin has a default rate limit for auth endpoints. You must configure rate limiting explicitly for your API.

How do I block specific IP addresses?

You can block IPs at the server level (firewall, nginx) or create a custom middleware that checks ctx.ip against a blocklist stored in environment variables or a configuration file.

Does Strapi support HTTPS natively?

Strapi can serve HTTPS directly, but it is recommended to use a reverse proxy (nginx, Caddy, Cloudflare) for HTTPS termination. The reverse proxy handles SSL certificates and offloads encryption work from Node.js.

How do I secure file uploads?

Validate file types and sizes on the server, use signed URLs for private files, scan uploads for malware with antivirus software, and store uploaded files in separate storage (S3, Cloudinary) with restricted access policies.

Mini Project

Your task: Implement a complete security configuration for a production Strapi API.

  1. Configure CORS to allow only your frontend domain (use http://localhost:3000 for testing).
  2. Implement rate limiting with koa-rate-limit: 60 requests per minute per IP.
  3. If using GraphQL, set max depth to 5 and disable introspection.
  4. Create a custom security middleware that adds security headers and logs suspicious requests.
  5. Configure environment-specific security settings using .env variables.
  6. 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:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro