Webbo3 Level 2 · JavaScript Development · Day 17 of 20
Security for Frontend Developers
Not a security course. The specific mistakes a junior is expected not to make, and why each one matters.
By the end of today you can
- Explain XSS and stop it in your own code
- Know exactly which values are safe to put in frontend JavaScript
- Treat browser validation as a courtesy and never as a defence
- Say what authentication and authorisation each mean
- Handle a secret you have already committed
You are not going to become a security engineer today. You are going to stop making five specific mistakes that appear in almost every beginner project.
Each of them has caused real incidents at real companies. Stopping all five is a realistic and genuinely useful goal for one day.
1. The One Rule Underneath All of It
Definition
Untrusted input is any value your code did not create itself: form input, URL parameters, API responses, anything in localStorage. All of it can be changed by whoever is sitting at the browser, including somebody who is not the user you imagined.
Real-Life Example
A bank teller does not pay out because a note says "please give the bearer ₦500,000". The note came from outside the bank, so it is checked, however official it looks.
Everything else today is this sentence applied to one specific place.
The address bar is outside your code. Here is a welcome page that reads the visitor's name from the URL. new URLSearchParams(location.search) reads the part of the address after the ?, and .get('name') gets one value from it.
Nobody typed anything. Clicking a link was enough to run code on your page.
Picture the bank teller holding a note from a stranger. However official it looks, it gets checked before anything is paid out.
2. XSS: The One That Will Actually Happen to You
Definition
Cross-site scripting (XSS) is putting untrusted text somewhere the browser treats it as code. You met it on Day 4. Today you see exactly why it is serious.
Real-Life Example
A shop notice board where anyone can pin a note. XSS is a note that, when a customer reads it, quietly takes the keys out of their pocket. The reader did nothing except read.
Why It Is Serious
The injected code runs with every privilege of whoever is looking at the page. It can read what your page can read, including a login token in localStorage, and send requests as that person. On an admin's screen it can do anything an admin can do.
The broken image is all anyone sees. A real attacker replaces console.log with a fetch to their own server, and every reader's token arrives there.
The Fix, and It Is One Word
textContent puts characters on the page as characters. The browser never reads them as HTML, so nothing in them can run.
Types: Where Untrusted Text Can Go
| Sink | Safe with user input | Note |
|---|---|---|
textContent | Yes | Use this by default |
innerHTML | No | Only for markup you built yourself |
insertAdjacentHTML | No | Same risk as innerHTML |
setAttribute("href", x) | No | A URL runs on click |
eval, new Function | Never | There is no safe version |
document.write | Never | Obsolete as well as unsafe |
The href row surprises people. A link's address can start with javascript:, and clicking it runs the rest as code. Here a student profile lets the user set their website.
The fix is to accept only web addresses. new URL(text) reads an address into its parts and throws if it is not a full address; its .protocol is the part before the //, such as 'https:'.
A comment box that allows bold and links is a real requirement. The answer is never your own regular expression: you will miss something, everybody does. Use a maintained sanitiser such as DOMPurify, and let it decide what survives.
Add a record to your Day 15 project whose name is <img src=x onerror="alert(1)">. If a popup appears, you have an XSS hole. Find the innerHTML it went through and switch it to textContent.
Picture the notice board note that picks the reader's pocket. textContent pins every note behind glass: readable, and unable to reach anybody.
3. Anything in Your Frontend Is Public
Definition
Every line of JavaScript your page runs is downloaded to the visitor's computer, where anyone can read it in DevTools. There is no such thing as a secret in browser JavaScript. Minifying does not help. Obfuscating does not help.
Real-Life Example
Writing your ATM PIN on the back of your card in tiny handwriting. Smaller writing does not make it a secret; it just takes a stranger a second longer to read.
Types: What May Go in the Frontend
| Value | Frontend? | Because |
|---|---|---|
| A publishable key, such as a Stripe publishable key | Yes | Designed to be public; it cannot do damage alone |
| A map key "restricted by domain" | Usually | Public, but limited to your domain. Set the restriction |
| A secret API key | No | Bills you, reads data, cannot be undone |
| A database password | No | Never leaves a server |
| An admin token | No | Full access to everything |
Secrets for tools that run on your machine go in a file called .env, which git is told to ignore with a .gitignore file. A .env.example with the names but no values is committed, so a teammate knows what they need.
If You Have Already Committed a Key
Deleting it in the next commit does nothing, because git keeps every earlier version. git show HEAD~1:app.js prints app.js as it was one commit ago.
Public repositories are scanned automatically within minutes. Go to the provider, revoke the old key and issue a new one. Rewriting history is optional; revoking is not, and it is the only step that actually helps.
The honest architecture: a key that must stay secret lives on a server you control, your frontend calls your endpoint, and your endpoint calls the third party. That is Level 3. Until then, prefer APIs that need no key.
Picture the PIN on the back of the card. If anyone has seen the card, you do not rub the number out: you change the PIN.
4. Browser Validation Is a Courtesy
Definition
Client-side validation is checking input in the browser, as on Day 13. It runs on the user's computer, under the user's control, so it helps honest users and stops nobody else.
Real-Life Example
A "please queue here" sign. It helps everyone who wants to queue properly, and it has never once stopped someone who does not.
Types: Three Ways Round It
- Open DevTools, delete the
requiredorminattribute, submit. - Change a hidden input, such as a price.
- Ignore the page entirely and send the request straight from the Console.
Validate in the browser so honest users get fast, kind feedback. Validate on the server because that is the only check that cannot be removed. Any rule that matters, a price, a permission, a limit, is enforced on the server or it is not enforced.
Picture the queue sign: kind to the people who read it, and invisible to the one who does not.
5. Authentication and Authorisation
Definition
Authentication is proving who you are. Authorisation is deciding what you are allowed to do. They are different checks, and a site needs both.
Real-Life Example
At an office building, the security desk checks your ID: that is authentication. Your access card opens the third floor and not the server room: that is authorisation.
| Authentication | Authorisation | |
|---|---|---|
| Question | Who are you? | What are you allowed to do? |
| Proof | Password, token, one-time code | Roles and permissions |
| Status code | 401 | 403 |
| Failure looks like | Anyone can log in as anyone | A student opens the admin page |
Hiding a Button Is Not Authorisation
This page hides the Delete button from a student. The practice API behind it, like a badly built real one, deletes for anyone who asks. The student copies the request from the Network tab and sends it from the Console.
The button was hidden; the capability was not. Hiding controls is user experience. Checking the permission on the server, and answering 403, is security.
localStorage is readable by any JavaScript on the page, as the comment in Section 2 proved, so one XSS hole hands over every logged-in session. The safer arrangement is an httpOnly cookie, which JavaScript cannot read at all. That is a server decision and therefore Level 3, but know the trade-off now, because every tutorial you read will offer you the localStorage version.
Picture the office: the desk checks your ID, the card decides which doors open, and taking the sign off the server-room door does not lock it.
6. Two More Worth Ten Minutes
Dependencies
Every package you install runs with your privileges and brings its own dependencies. Real attacks have come through a package that a package that your package depends on.
- Check the weekly downloads and the last publish date before installing anything.
- Read the name twice. Typosquatted packages differ from a popular one by one character, on purpose.
- Ask whether the browser already does it. Day 12 was largely that question.
HTTPS
Over plain http, anyone between your user and your server can read and change everything, including injecting scripts into your page. https is also required for geolocation, the clipboard, service workers and notifications. GitHub Pages, Netlify and Vercel all give you https for nothing, so there is no remaining excuse.
Do This Now
- Add
<img src=x onerror="alert(1)">as a record in your Day 15 project. If you get a popup, find the hole and fix it with textContent. - Search your whole project for innerHTML. For every hit, write down where that string came from. Any that traces back to a user gets changed.
- Open your deployed project, delete a required attribute in DevTools and submit. Watch your own validation disappear.
- Run npm audit if you have a package.json. Read one advisory in full.
- Check every repository you pushed this month for a committed key. If you find one, rotate it today.
Checkpoint
Q1. Why is XSS worse than "somebody can make my page look odd"?
The injected script runs with the privileges of whoever is viewing the page. It can read their token and storage, make requests as them, and on an administrator's screen do anything an administrator can do.
Q2. You committed an API key and deleted it in the next commit. Safe?
No. It is in the history, as git show HEAD~1 proved, and public repositories are scanned within minutes. Rotate the key at the provider.
Q3. Your form checks the price is above zero. Is the price safe?
No. The check runs in a browser the user controls, and removing min took one line. Prices are validated on the server or not at all.
Q4. The Delete button is hidden for non-admins. Is deletion protected?
No. Hiding a control is user experience. If the endpoint accepts the request from anyone, anyone who opens the Network tab can send it.
Tick Before You Move On
- ☐ I tested my own project with a hostile string and it did not execute
- ☐ Every innerHTML in my code is markup I built, not user text
- ☐ Every user-supplied link goes through a check like safeLink()
- ☐ No key of any kind is in my committed code, and .env is ignored
- ☐ I can say what 401 and 403 each mean without looking it up
- ☐ My deployed project is served over https
Quick Recap
Never trust anything that came from outside your code. textContent for user text; a real sanitiser when you genuinely need HTML. Anything in frontend JavaScript is public, so rotate a leaked key immediately. Browser validation is kindness; server validation is the defence. Hiding a button is not authorisation.
Final Scenario
Before your project goes live, you spend fifteen minutes attacking it yourself.
You post a comment that is an image tag and see plain text. You set your website to a address and the check refuses it. You search the history for "key" and find nothing. You remove the min attribute and note that the server must check the price too.
Fifteen minutes, five mistakes ruled out, and a project you can hand to anyone.
Tomorrow, Day 18: getting it off your laptop and onto a URL you can send to somebody.