CSRF Protection
Cross-Site Request Forgery (CSRF) is a vulnerability where a malicious website tricks a user’s browser into making an authenticated request to your application without the user’s consent.
Harpia provides a built-in CSRF class to generate and validate one-time tokens, ensuring that state-changing requests (POST, PUT, DELETE) originate from your own application.
When Do You Need CSRF?
CSRF exploits the fact that browsers automatically attach cookies to every request. This means it is only relevant when your application uses cookie-based authentication (sessions or auth cookies).
If your API uses Bearer tokens sent in the Authorization header, CSRF is not a concern because a cross-site attacker cannot read tokens from memory or localStorage and inject them into request headers. However, this is a security trade-off: the architecture simply shifts your priority from CSRF to XSS (Cross-Site Scripting), since any injected script can trivially read and exfiltrate tokens stored on the client.
Bearer Tokens & Security
No CSRF risk, but high XSS risk. Prioritize sanitizing inputs and auditing dependencies.
Configuration
Instantiate the CSRF class. By default, it uses an in-memory store and tokens are valid for 5 minutes.
import { CSRF } from "@harpiats/core";
const csrf = new CSRF();
You can customize the storage mechanism (e.g., RedisStore) and the TTL.
const csrf = new CSRF({
store: new RedisStore(),
ttl: 15 * 60 * 1000 // 15 minutes
});
Usage Flow
The core concept is always the same, regardless of your authentication method: associate the CSRF token with a unique, stable identifier for the current user/request.
Using Session-Based Authentication
If you use Harpia’s Session, the session ID is the natural choice as the key.
app.get("/profile/edit", async (req, res) => {
const sessionId = await req.session.id();
const csrfToken = await csrf.generate(sessionId);
await res.render("profile-form", { csrfToken });
});
app.put("/profile", async (req, res) => {
const sessionId = await req.session.id();
const formData = await req.formData();
const token = formData.get("csrf_token") as string;
const isValid = await csrf.check(sessionId, token);
if (!isValid) return res.status(403).json({ message: "Invalid CSRF token" });
// Proceed with the update...
});
Using Cookie-Based Authentication
If you are using a persistent auth cookie (e.g., a user ID stored in a cookie after login), use that value as the key.
app.get("/settings", async (req, res) => {
const userId = req.cookies.get("user_id"); // Your auth cookie
const csrfToken = await csrf.generate(userId);
await res.render("settings", { csrfToken });
});
app.post("/settings", async (req, res) => {
const userId = req.cookies.get("user_id");
const formData = await req.formData();
const token = formData.get("csrf_token") as string;
const isValid = await csrf.check(userId, token);
if (!isValid) return res.status(403).json({ message: "Invalid CSRF token" });
// Proceed...
});
2. Injecting the Token into HTML Forms
Regardless of the identifier used, the HTML side is always the same: include the token as a hidden input field.
<form method="POST" action="/settings">
<input type="hidden" name="csrf_token" value="{{ csrfToken }}">
<!-- Rest of your form fields -->
<button type="submit">Save Changes</button>
</form>
Invalidating a Token
After successfully processing a request, you can call csrf.delete() to manually invalidate the token before its TTL expires. This enforces a strict single-use policy.
// After successfully saving, delete the token immediately
await csrf.delete(userId);
Token Expiration
CSRF tokens have an automatic TTL. If a user leaves a form open for longer than the TTL and tries to submit it, validation will fail. Ensure your TTL is long enough for standard user interaction.