A content management system (CMS) enables users to create, manage, and publish content online without having to code. It simplifies the publishing process and provides some advanced features out of the box, like allowing multiple authors to work on different content at the same time.
CMSs have been around for a long time, and much of the web runs on them. A typical CMS has a backend where admins post content, tightly coupled with a frontend where content is rendered. These systems aren’t flexible, though, and leave you with few options when it comes to configurations.
On the other hand, a headless CMS does not have a frontend. It is a backend-only CMS that makes content available over an API. Content can be fetched and displayed using any frontend. This provides a high level of flexibility to developers, who can choose which user interface (UI), user experience (UX), stack, and frameworks to use. The increased usage of headless CMSs has re-revolutionized the way CMSs are used.
That includes Strapi, a headless CMS first released in 2015 and created using Node.js and React. It supports the databases PostgreSQL, SQLite, MySQL, and MariaDB; delivers content through a REST API; and supports the GraphQL query language.
Strapi has grown in popularity because most developers already use Node and React, making its code base easy to modify. As Strapi usage rises, though, so do the security challenges. There are certain security measures you can take to ensure a safer experience. This post will provide a Strapi security checklist.
Using a headless CMS introduces unique security considerations. API calls in a headless CMS expose endpoints that can be targeted by malicious actors. Allowing applications from different domains to make requests to your app increases the risk of a security breach. Understanding these threats is the first step in securing your Strapi application.
SQL injection allows attackers to interfere with the queries your application makes to the database. By injecting malicious SQL code through unsanitized input fields, attackers can manipulate database queries, potentially accessing or modifying sensitive data.
CSRF attacks exploit the trust that a site has in a user's browser. An authenticated user's browser can be tricked into submitting unwanted requests to your application, leading to unauthorized actions.
In DoS and DDoS attacks, attackers overwhelm your server with traffic, rendering your application inaccessible to legitimate users. This not only affects performance but can also lead to financial and reputational damage.
Attackers use automated tools to guess passwords or API keys by trying many possible combinations. Weak passwords or API keys make your application vulnerable to unauthorized access.
In GraphQL APIs, if authorization checks are not consistently applied across all resolvers, attackers may exploit unprotected endpoints to gain access to restricted data.
Failure to implement rate limiting can expose your API to abuse. Attackers can bombard your server with requests, leading to resource exhaustion and potential downtime.
Certain key areas of Strapi require special care in order to prevent attacks. Here are some best practices for keeping your app secure.
Strapi configurations require credentials for sensitive information about the app. If these details are hacked, your app is at risk. Always use a .env
file to securely store credentials within the application.
Credentials include information like API keys, database usernames, and passwords. In addition to using .env
files, be sure not to do this:
Especially if your app is in production, this exposes sensitive information.
Create a .env
file at the root of your application and populate it with credentials. Below is an example of how data can be stored in a .env
file.
1DATABASE_PASSWORD=myPassword
You can easily get access to this data in the config file:
1module.exports = ({ env }) => ({
2
3 connections: {
4
5 default: {
6
7 settings: {
8
9 password: env('DATABASE_PASSWORD'),
10
11 },
12
13 },
14
15 },
16
17});
Check the official documentation for more information.
Validation verifies the authenticity of user input. Every input should be properly validated to ensure the correct data type and prevent the input of malicious codes. Strapi object-relational mapping (ORM) has an inbuilt validation implementation. But the wrong data type can cause a crash, and the error reporting can be faulty.
You can validate your inputs using joi, a JavaScript validation package. Its simple syntax allows you to validate your data in a readable way.
Install joi using npm:
1npm install joi
The documentation offers rich tutorials.
Sanitization removes any malicious code coming from the user’s input. Strapi uses ORMs like Bookshelf, which sanitizes data before storing. To be even safer, you can apply another layer of sanitization to your code before data reaches the Strapi ORM, especially if your input involves users entering HTML or coding languages.
sanitize-html is well suited for cleaning up HTML fragments and for removing unwanted CSS when copying and pasting from Word.
Roles and permissions determine which user has access to what route in the Strapi application. Strapi by default places restrictions on certain routes, but developers sometimes grant permissions to all routes:
A hacker can easily access these routes using a tool like Postman, Vanilla JavaScript, or any frontend framework of choice. Make sure that the role and permission of every route in your app is properly set up. You can go the extra mile by adding policies to each route.
Policies can be found in Strapi routes like so:
They are functions that run and execute certain logics before the request gets to the controller. Policies are created mainly to set restrictions on certain features in the application. For example, a policy can ensure a user is logged in before allowing them to write a post.
To create a policy, create a /policies
folder in ./api/**/config
of your project directory or plugins directory. You can also create a global policy in ./config/policies/
at the root of your application.
For example, say you create a global policy that ensures a user is logged in before carrying out any action. In /config/policies
create the JavaScript file name auth-check.js
and add the following:
1 module.exports = async (ctx, next) => {
2
3
4 if (ctx.state.user) {
5
6
7 return await next();
8
9
10 }
11
12
13 ctx.unauthorized(`You're not logged in!`);
14
15
16 };
This checks to see if the user has an active session. If it does not return true, the user will be notified that they are not logged in.
You can call the policy in the route like so:
1 {
2
3 "routes": [
4
5 {
6
7 "method": "GET",
8
9 "path": "/restaurants",
10
11 "handler": "Restaurant.find",
12
13 "config": {
14
15 "policies": ["global::auth-check"]
16
17 }
18
19 }
20
21 ]
22
23}
Policies are useful in Strapi and deserve to be studied in depth. For more details, check the documentation.
Data leakage is one of the most important factors in a security check. Sensitive user data stored in your application should not be released to an untrusted source, either intentionally or unintentionally.
In Strapi, a collection type can have a one-to-many or a many-to-many relationship with the user. Making a GET request to the collection type will return the user data as part of the object. This is a form of data leakage.
For example, say you have an article collection type belonging to a user. The user object contains sensitive details like username, address, and phone number. If you fetch the article, you will get the user details along with it:
To fix this, pass private: true
in the model of your collection type.
This restricts access to the user data but won’t break your code. If you make the request again, you will get your article data without the user data.
Securing your Strapi application is an ongoing process. Implementing effective security strategies helps protect your data and maintain user trust.
Regular Updates: Frequently update your application and dependencies.
Strong Password Policies: Enforce complex passwords and consider password expiry policies.
Two-Factor Authentication: Add 2FA to enhance account security.
Regular Backups: Maintain regular backups of your data to recover in case of an incident.
Security as a Priority: Integrate security considerations into every stage of development.
Training and Awareness: Educate team members about common threats like phishing and malware.
Secure Coding Practices: Encourage best practices such as input validation and output sanitization.
Despite best efforts, security incidents can happen.
Response Plan: Have a clear plan in place for identifying, reporting, and responding to security incidents.
Post-Incident Review: After any incident, conduct a thorough review to understand what happened and how to prevent similar issues.
The security of your Strapi application is just as important as every other feature. In today's digital landscape, cyber threats are constantly evolving, and staying vigilant is essential. By following this security checklist and implementing best practices, you can protect your application from potential vulnerabilities and ensure greater success for you and your business.
Take the time to secure your app, keep your software up to date, and foster a security-first culture within your team. Your users and your business will thank you.
Anumadu is a fullstack engineer, but also a technical writer specialized in Laravel, Vue.js, Nuxt.js and Node.js.