This episode covers securing a Remix application: secure headers and content security policy, preventing XSS, CSRF, and injection, server-side input validation with sanitization, and handling secrets and credentials safely.

The authentication from episode 11 opens your application's front door. But a door alone isn't enough — the windows, vents, and basement must be checked too. Episode 12 is a thorough security audit for your Remix application.
The good news: Remix is already secure by default in many ways. React rendering escapes automatically, so XSS from ordinary data is rare. Form-based actions use sameSite cookies to fight CSRF. But good defaults aren't a reason to let your guard down — security is a layer you build deliberately.
Episode 12 covers secure headers and CSP, preventing XSS, CSRF, and injection, input validation and sanitization, and correct secret handling.
HTTP headers signal the browser to block dangerous behavior. In Remix, headers are set through the headers function on a route, or better yet, globally in the root:
export function headers() {
return {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=()",
};
}These headers block framing, prevent MIME sniffing, and limit the data sent via the referrer. Start with these four — all supported by modern browsers without much risk.
CSP provides a list of allowed sources for scripts, styles, and assets. It's the strongest defense against XSS. A strict CSP needs to be relaxed during development because Vite injects scripts for hot reload.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'Start strict, then loosen only what the application actually needs. Inline scripts and eval should be avoided in production.
React renders text with automatic escaping — user text won't be executed as HTML. The danger appears when you deliberately use raw HTML through dangerouslySetInnerHTML. If you must, make sure the content is sanitized on the server with a library like sanitize-html.
import sanitizeHtml from "sanitize-html";
export async function action({ request }) {
const formData = await request.formData();
const konten = sanitizeHtml(String(formData.get("konten")));
await simpan(konten);
return redirect("/posting");
}Sanitization happens on the server before the data is stored, not on the client. The rule: clean input at the point of entry, not at the point of exit.
Remix uses cookies with sameSite: "lax" from episode 11, which blocks most CSRF attacks. An extra layer: validate the Origin header on sensitive actions. Check that the request comes from your application's domain before processing data changes.
Injection happens when user input is concatenated directly into a query. ORMs like Prisma use parameterized queries internally, so they're safe — as long as you don't write raw SQL with string concatenation. Never concatenate user input directly into an SQL query.
Every incoming input must be validated on the server: length, type, and format. Libraries like Zod provide schemas reusable between validation and typing. The schema is parsed against the incoming data; the result is safe to use.
import { z } from "zod";
const schemaPost = z.object({
judul: z.string().min(3).max(100),
konten: z.string().min(10),
});
export async function action({ request }) {
const formData = await request.formData();
const hasil = schemaPost.safeParse(Object.fromEntries(formData));
if (!hasil.success) {
return { errors: hasil.error.flatten().fieldErrors };
}
await simpan(hasil.data);
return redirect("/posting");
}safeParse doesn't throw; hasil.success indicates whether the input is valid or not. Validated data is safe to store in the database.
Besides format, limit input size and field types. File uploads have their own size limits — don't let a single request swallow the server's memory. Rate limiting and maximum request sizes are important infrastructure defenses.
All secrets — API keys, database URLs, session secrets — must live in server environment variables. Never write them in code or send them to the client. Remix never ships server code to the browser; make sure secrets are only read in loaders and actions.
Separate secrets per environment: development, staging, and production each use different values. Rotate secrets regularly, especially if you suspect a leak. Document where each secret is used so rotation doesn't break other services.
Episode 12 builds layered defenses: secure headers and CSP, XSS prevention through escaping and sanitization, CSRF defense via sameSite and origin checks, parameterized queries against injection, input validation with Zod, and strict secret management. Security isn't a feature — it's a habit.
The key takeaways:
In the next episode, episode 13, we'll discuss API integration and external data — fetching from external APIs in loaders and actions, working with REST and GraphQL backends, handling auth tokens securely, and rate limiting with error fallbacks. Internal security is handled; now it's time to talk to the outside world.