What is a security audit?
An audit is a planned read-through of a whole site, looking for gaps before an attacker does. It differs from the earlier lessons in one way: nobody tells you where the problem is. You start with the full site and a checklist, and you work out what needs fixing.
For Spoke and Chain the scope is the files in the lab: the server code, the headers, the settings and the package list. A real audit also looks at the hosting account, who has admin access, backups, the domain registrar and the email setup, because attackers use whichever door is open.
How should you start?
Run everything once and write down what you see before changing anything. In the Attacker view, press Try it on every attack. Seven get through: the quote trick on the repairs search, a cross-site read of the account API, a forged hire booking, an edited hire price, a key in the code, a package with an advisory and a package that does not exist.
Then sort them. Some share a cause, and fixing the cause fixes several at once. Some are one-line settings. Some need a small code change. Starting with the shared causes saves work and avoids fixing the same problem twice in different ways.
Note what is already right as well. The sign-in route has a rate limiter, issues a new session, compares passwords with bcrypt, and two-step sign-in is required. An audit report that lists strengths helps the owner know what not to undo.


What went wrong with the account API?
The shop's hire app lives on another address and needed to read /api/account. Someone hit a CORS error in the browser and fixed it by copying whatever Origin arrived into Access-Control-Allow-Origin, with Access-Control-Allow-Credentials: true in headers.txt. The login cookie was then set with sameSite: 'none', because otherwise the app's requests arrived signed out.
Each step made the error go away. Together they mean any site, including evil.example, can fetch a signed-in rider's account and read the answer. The Network view showed the cross-origin fetch, the copied origin, and the browser handing over the response.
The fix depends on what the hire app needs. If it is served from the shop's own address, it needs no CORS at all. If it really lives on another origin, name that one origin exactly, check it against a fixed list in code, and keep the cookie SameSite=Lax where you can. An app on a different site will not receive a Lax cookie, so it usually signs in with its own token instead. In the lab, removing the copying code and the credentials line, and setting the cookie back to lax, closes both the cross-site read and the forged booking.
// Remove this: it trusts every site
res.set('Access-Control-Allow-Origin', req.headers.origin);
// Cookie: keep other sites' requests signed out
res.cookie('sid', req.session.id, { httpOnly: true, secure: true, sameSite: 'lax' });What about the repairs search and the hire price?
The repairs page lets a customer look up their bike by surname. The query glues the surname into the SQL with +, so a surname containing a quote changes the query. The output on that page was escaped correctly; only the query was wrong. A ? placeholder and a values list fix it, and a real customer called O'Brien can now find their bike, which the old version would have broken on.
The hire booking saves req.body.cost, a number the browser chose. The fix is the same as the bakery's order page: look the bike's day_rate up from the database and use that. Watch for the same pattern anywhere money or a quantity appears.
What about the key and the packages?
The SMS token for sending hire reminders is written into server.js. It moves to .env, the code reads process.env.SMS_TOKEN, and .gitignore lists .env. In a real audit you would also ask whether this file was ever pushed, and if so revoke the token at the SMS provider today.
@pretend/uploader 2.0.1 has an advisory about saving files outside the uploads folder; 2.0.4 fixes it. bike-size-helper is not a real package. Ask where it came from. If an assistant suggested it and nobody checked, find out what it was meant to do, and replace it with a few lines of your own or with a known package.
How do you check the fixes did not break anything?
Run every attack again, and then use the site as a customer would. In the lab that means the scoreboard shows nothing getting through and the Page view shows no warnings. On a real site it means signing in, searching repairs, booking a hire and checking the account page, ideally in two browsers.
The CORS change deserves particular care. If the hire app really needed cross-origin access, it will stop working after your fix. That is the right outcome for an unsafe setup, but tell the owner before the change goes live and agree on the safe version, so nobody undoes your fix in a hurry next week.
How do you write up an audit?
Keep it short and specific. For each problem: what it is in one sentence, which file and line, what an attacker could do, how serious it is, and what you changed or recommend. Put the serious ones first. A rider's account readable by any website is serious. A missing Referrer-Policy is minor.
Add the strengths and the things you could not check, such as hosting access or backups. Date the report. The next audit starts from it, and the broken version of this lesson shows why a next audit is always needed: changes made a month later undid three of the defences.
What do AI tools get wrong in audits?
Ask an assistant to audit a file and it will often return a long generic list, much of it not about your code. It may flag things that are fine, miss the reflected Origin because it is in middleware rather than a route, or recommend outdated fixes such as X-Frame-Options: ALLOW-FROM and the csurf package. Use it to suggest places to look, then confirm each finding yourself against the code.
Where does the lab simplify?
The lab knows fifteen attacks and reads the files with pattern rules. A real audit also covers access control, such as whether one rider can view another rider's hires by changing an id, file uploads, error messages that reveal too much, logging, and the hosting setup. Real attackers try many payloads and chain small problems together. Treat a fully green Report as a strong start on a real site, and for anything handling payments or personal data, consider a paid professional review.
Field notes
- An audit checks a site against a list and records each problem with its fix.
- Start with the Report view and the checklist, then work through one finding at a time.
- Fix the account API with an exact CORS allowlist and SameSite=Lax cookies.
- Test that each fix kept the site working, then write up what you found and changed.
Easy to get wrong
Copying the Origin back trusts every site on the internet, including evil.example.
The SameSite=None cookie caused two of the seven problems. One change closes both.
The slowness is the defence. Each sign-in takes a fraction of a second; each guess for an attacker takes the same.
Questions people ask
Can I audit someone else's site?
Only with written permission from the owner, and only the parts they agree to. Testing a site without permission is illegal in many countries, even with good intentions.
How long does an audit of a small site take?
A site this size takes an afternoon to read through carefully with a checklist, plus time for fixes and testing. Larger sites take longer, mostly because there are more routes to follow.
What if I find a problem I cannot fix?
Write it down with how serious it is, and reduce the damage where you can, for example by turning a feature off until it is fixed. Hiding it from the owner helps nobody.
What should I learn after this course?
Access control, meaning who may see and change which records, is the next big topic. File uploads, logging and incident response follow. Keep the checklist from the previous lesson and add to it as you learn.