Security Hardening in Server Actions
Explore essential techniques to harden Server Actions by implementing user authorization, rate limiting with Upstash, and leveraging Next.js and Prisma's built-in protections against CSRF and SQL injection. This lesson helps you understand how to secure data mutations and prevent abuse in full stack applications.
So far, we’ve focused on using Server Actions to make our forms functional. Now, we need to make them secure.
Because Server Actions run on the server and can modify the database, they are a primary target for attacks. Hardening our Server Actions means adding essential security layers to protect against common vulnerabilities.
In this lesson, we’ll apply common authorization and rate-limiting patterns to Server Actions and see how modern frameworks handle threats like CSRF and SQL injection.
The number one priority: Authorization
It’s not enough to know who the user is (that’s authentication); we also need to confirm they’re allowed to do what they’re trying to do. For example, anyone can try to delete a post, but only the author should be able to. Authorization is the process of checking whether a user has permission to perform an action on the data.
To show this, we’ll use a mock function to simulate getting a logged-in user. In a full application, this would involve reading a secure session cookie, but this lets us focus purely on the authorization logic itself.
// lib/auth.js/*** Simulates fetching a logged-in user.* In a real application, this would involve verifying a session cookie and retrieving user ID.*/export async function getMockUser() {return { id: 'user_1' };}
Now, let’s secure a deletePost Server Action.
const { PrismaClient } = require('@prisma/client');
const db = new PrismaClient();
async function main() {
// Create (or update) two users
// We use 'upsert' so this script can run multiple times without failing on "Unique constraint"
await db.user.upsert({
where: { id: 'user_1' },
update: {},
create: { id: 'user_1', bio: 'Mock user 1' },
});
await db.user.upsert({
where: { id: 'user_2' },
update: {},
create: { id: 'user_2', bio: 'Mock user 2' },
});
// Create a post that specifically belongs to 'user_1'
// This establishes the "Owner" relationship we need to test authorization logic later
await db.post.upsert({
where: { id: 'post_1' },
update: {},
create: {
id: 'post_1',
content: 'This is a seeded test post',
authorId: 'user_1'
},
});
console.log('Seeded successfully!');
}
main()
.finally(() => db.$disconnect());Explanation:
Lines 10–12: We first ensure a user is “logged in.” If not, we stop everything.
Lines 15–17: We fetch the resource the user is trying to affect.
Lines 24–26: We then verify ownership of the ...