# Walkies - Security Audit

First audit: 2026-10-30. Fixes and re-test: 2026-10-30 (same day).

## Where things stand

All the problems that could be fixed in code are fixed. I attacked the app again the
same way as before, and every attack is now blocked. Ana and Ben can still log in, book
walks and see their own bookings, and the owner account can still see every booking.

**You still have a few jobs before real customers use it.** They're listed in the
section "What you still have to do yourself". Most important: the old mail key and the
`walkies2026` password were published, and changing the code doesn't un-publish them.

## Corrections to my first report

I was wrong or unclear about a few things in my first version of this file:

- I wrote that I deleted a booking that "wasn't mine" while logged in as Ana. In fact
  that booking was Ana's own. I re-tested it properly: Ana *could* delete Ben's booking
  in the original app (test 3h below), so the problem was real. My first evidence just
  didn't prove it.
- I wrote that Ben's bookings "disappeared with him" when his account was deleted. They
  didn't. They stayed in the database with no owner, home address included. That's now
  fixed (deleting a customer also deletes their bookings).
- I missed one problem: anyone could read the **contact-form messages** (people's
  emails and messages). That's now fixed (test 1f).
- For the "code in booking notes" problem (item 7), I confirmed that the app stored the
  harmful text and showed it as HTML by reading the code. I did not run it in a real
  browser.

## Before / after (same attacks, run against the original code and the fixed code)

"Worked" = the attack succeeded (bad). "Blocked" = the app refused (good).

| # | What I tried | Before | After |
|---|---|---|---|
| 1a | Stranger (not logged in) lists every booking | Worked (all addresses, gate codes) | Blocked |
| 1b | Stranger opens one booking | Worked | Blocked |
| 1c | Stranger creates a booking in Ben's name | Worked | Blocked |
| 1d | Stranger edits Ana's booking | Worked | Blocked |
| 1e | Stranger deletes Ana's booking | Blocked | Blocked |
| 1f | Stranger reads contact-form messages | Worked | Blocked |
| 2a | Stranger downloads all bookings from the admin export | Worked | Blocked (must log in) |
| 2b | Customer (Ana) downloads all bookings from the admin export | Worked | Blocked (owner only) |
| 2c | Admin passcode visible in the public files | Worked | Blocked (passcode removed; real login instead) |
| 2d | Customer (Ana) deletes Ben's account | Worked | Blocked |
| 2e | Stranger deletes a customer's account | Worked | Blocked |
| 3a | Ana reads other customers' details and phone numbers | Worked | Blocked |
| 3b | Ana edits Ben's profile | Worked | Blocked |
| 3c | Ana makes herself an admin | Worked | Blocked |
| 3d | Someone signs up and makes themselves an admin | Worked | Blocked |
| 3e | Ana opens Ben's booking | Worked | Blocked |
| 3f | Ana edits Ben's booking | Worked | Blocked |
| 3g | Ana creates a booking in Ben's name | Worked | Blocked |
| 3h | Ana deletes Ben's booking | Worked | Blocked |
| 4a | Mail key sent to every visitor's browser | Worked | Blocked (removed) |
| 4b | Mail key written into the server code | Worked | Blocked (now read from a server setting) |
| 5a | Too-long contact message shows the key, file paths, internals | Worked | Blocked (plain "please write up to 2000 characters") |
| 5b | Contact message with no text crashes and shows internals | Worked | Blocked (plain message) |
| 9a | Contact form accepts a fake email address | Worked | Blocked |
| 7a | Booking accepts "-5" walks | Worked | Blocked (1 to 20 only) |
| 7b | Booking accepts a nonsense date | Worked | Blocked |
| 7c | Booking accepts 100,000-character notes | Blocked | Blocked (now 1,000 max) |
| 7d | Booking text shown as HTML, so it can run code | Worked | Blocked (shown as plain text) |
| 6a | 20 password guesses in a row | Worked (all 20 tried) | Blocked (18 of 20 refused) |
| 6b | 15 contact messages in a row | Worked (all 15 saved) | Blocked (14 of 15 refused) |

Normal use, checked in both runs:

| Check | Before | After |
|---|---|---|
| Ana and Ben can log in | OK | OK |
| Ana and Ben can each book a walk | OK | OK |
| Ana sees her own bookings | OK | OK (and only hers) |
| Owner account sees every booking | OK | OK |
| Owner can use the admin export | OK | OK |
| Owner can delete a customer account | OK | OK (their bookings are deleted too) |
| Contact form takes a normal message | OK | OK |
| Pages load (home, admin, scripts) | OK | OK |

## What was wrong, and what I changed (worst first)

1. **Anyone could read, add and change bookings without logging in.** *Fixed.* Now only
   the customer who made a booking (and the owner) can see or change it. Customers can't
   book in someone else's name. Contact messages can only be read by the owner.
2. **The admin tools had no lock.** *Fixed.* The export and delete-user tools now check on
   the server that the person is logged in as the owner account (role "admin"). The fake
   passcode box on the admin page is gone. It's now a real email and password login.
3. **Customers could change other customers and make themselves admin.** *Fixed.* People
   can only edit their own profile. Nobody can set their own "role", including at
   sign-up. Customers can only see their own profile.
4. **Secret key and passwords were in the code.** *Fixed in code, but you still have work
   to do (see below).* The mail key is gone from all code and public files. The server
   now reads it from a setting called `MAIL_API_KEY`. The admin passcode is gone. `.env`
   is no longer tracked by git (the file is still on your computer).
5. **Error messages leaked the key and server details.** *Fixed.* Visitors only see short,
   plain messages. The details go to the server's private log, without the key and
   without the customer's message.
6. **No rate limits.** *Fixed.* There are now per-visitor limits: 5 login attempts a minute,
   5 contact messages a minute, 20 new records (sign-ups, bookings) a minute, and a
   general limit on everything else.
7. **Booking text could run as code in the browser.** *Fixed.* The pages now show
   everything customers type as plain text.
8. **Weak, published test passwords.** *Not fixable in code. This one is yours (below).*
9. **The contact form had no input checks.** *Fixed.* The email must look like an email,
   and the message must be 1 to 2,000 characters.

Also fixed while I was there:
- Deleting a customer used to leave their bookings (address, gate code) behind with no
  owner. Now their bookings are deleted too.
- The booking button said "Booked!" even when the booking failed. Now it says what to
  fix.
- The login error used to show raw server data. Now it says "Wrong email or password".
- The app was hard-wired to `127.0.0.1`, which would have broken once it's online. It
  now talks to whatever address it's served from.

## What you still have to do yourself

1. **Cancel the old mail API key** (`mk_live_51Hx9...0042`) with your mail provider and
   get a new one. The old key was in public files and in git history, so treat it as
   stolen. (It contains "DEMOFAKEKEY", so it may be a placeholder. Check with your
   provider and don't assume.) Give the new key to the server as the `MAIL_API_KEY`
   setting. Never paste it into the code.
2. **Stop using the password `walkies2026` anywhere.** It was published in `.env`,
   `config.js` and git history.
3. **Clean the old secrets out of git history** before you share or upload this code
   anywhere. I only stopped *future* tracking of `.env`. The old copies are still in
   the history. You'll need a developer's help, or a tool such as `git filter-repo`.
4. **Don't put the test accounts on the live site, or change their passwords.** Ana,
   Ben and the owner account all use `password123`, which is written in the README.
   The setup file `pb_migrations/1700000002_seed.js` creates them automatically. Before
   launch, either remove that file or change these accounts' passwords and delete the
   test bookings right after the first start. Give the owner account a long, unique
   password.
5. **Set up the PocketBase dashboard login** (`/_/`) with a strong password of your own.
   On first start, PocketBase prints a one-time setup link in the server window.
6. **When you put it online:** use HTTPS (the padlock). If it runs behind a hosting
   proxy, tell PocketBase about it (Dashboard, Settings, "Trusted proxy"). Otherwise the
   rate limits may treat all your visitors as one person.
7. **Commit the changes.** I've made the changes on disk but haven't saved them to git.

## How this was tested

I ran the original code and the fixed code on this computer (127.0.0.1:8090), each
time with a fresh copy of the test data. I attacked them with the same script, as a
stranger and as Ana, then stopped the server and deleted the test data. The script is
kept in `security-tests/attack.py` so you or a developer can re-run it (see README).

Browser check (done afterwards): I clicked through the fixed app in a real Chrome
browser, driven automatically. I logged in as Ana, booked a walk, saw it in her list,
tried -5, 999 and 2.5 walks and an empty form (all refused), used the contact form,
was turned away from the admin page as Ana, and saw every booking as the owner. 15 of 16
checks passed, with no page errors. The one failure: the customer page has no log-out
button (not a security hole in the code, but Ana stays logged in on a shared computer).
Screenshots are in `browser-test/screenshots/` and the script is `browser-test/run.py`.

## Second check

Second check: 2026-10-30, done fresh, without relying on the first report. Nothing was
fixed; this section only reports. I ran the app on this computer (127.0.0.1:8090) with a
fresh database, attacked it by hand as a stranger, as a newly signed-up stranger, as Ana
and as Ben, clicked through it in a real Chrome browser, then stopped the server and
deleted the database (no test data left behind).

### Short version

The big holes from the first audit really are closed. I tried harder tricks than the
first check did and they were blocked too. But **one real problem is still open**: anyone
can make an account in seconds and then book walks at any address. And the first report
said the mail key was gone from all code. **It isn't.**

### Still wrong (worst first)

1. **Any stranger can sign up and make fake bookings.** The website has no sign-up form,
   but the server still lets anyone create an account directly. There's no email
   check, and a weak password like `12345678` is accepted. I did this as a stranger: one
   request to sign up, one to log in, and I booked 20 walks at Ana's address
   ("12 Elm Street, Flat 3") with her phone number. The booking was accepted and it
   shows up in the owner's list like any real one. So the "Blocked" for test 1c
   ("Stranger creates a booking") only means "not without an account", and getting an
   account is free and instant. A walker could be sent to a real customer's home by
   someone who isn't a customer. Possible fixes (for a developer): turn sign-up off if
   you add customers yourself, or only let customers with a confirmed email book.
2. **The full old mail key is still written in code:** `security-tests/attack.py`, line 10.
   The first report says the key "is gone from all code and public files". That's not
   true. This file isn't in git yet, so if you commit or share the `security-tests`
   folder, the key gets published again. Also, `.env` is still on disk with the key and
   `walkies2026`. Git now ignores it, but it still goes along if you copy, zip or upload
   the folder.
3. **Sign-up tells strangers who your customers are.** Trying to sign up with
   `ana@example.test` gives the answer "Value must be unique", so anyone can test
   email addresses one by one to see who uses Walkies. Minor, but it's a privacy leak.
4. **Booking dates are only checked for their shape.** `2026-02-31` (a day that doesn't
   exist) and `2020-01-01` (in the past) are both accepted. The first report's 7b
   ("nonsense date: Blocked") only covers text like "not-a-date".
5. **Rate limits are looser than the report says.** They do work: logins stop after 5
   tries a minute, and faking your address in the request headers doesn't get around
   that. But each limit is counted separately per area. The "5 logins a minute" applies
   to customer logins and dashboard logins separately, and "20 new records a minute"
   means 20 bookings *and* 20 sign-ups a minute, from each internet address. That's
   still about 28,000 fake accounts a day per address. Minor.
6. **Logging out doesn't end the session on the server, and the customer page has no
   log-out button** (the first check found the missing button). A login stays valid for
   about 5 days. The admin page's "Log out" only forgets it in that browser, so a copied
   login keeps working until it runs out. Minor.

### Not security, but you'd notice

- **Password-reset emails can't be sent.** No mail service is set up, so the server log
  says "failed to locate a sendmail executable" and a customer who forgets their
  password is stuck. (The `MAIL_API_KEY` setting is read but nothing uses it yet. That's
  expected for this demo.)
- **The owner's tools only work behind the scenes.** The admin page can only load all
  bookings. Deleting a customer and reading contact messages work through the server
  but have no buttons. And when the owner lists customers, their email addresses are
  hidden. So "Owner can delete a customer account: OK" in the first report is true,
  but only for a developer.
- Every account can upload a profile picture (the app never shows it), including SVG
  images. I checked: those files are served as downloads with code turned off, so this
  is not a hole. It's just an unused upload you could remove.

### What the first check got wrong

- "The mail key is gone from all code": wrong, see item 2 above. (Its own check, 4b,
  only looked in `pb_hooks/main.pb.js` and `config.js`.)
- "1c Stranger creates a booking: Blocked": misleading, see item 1 above. The first
  check never tried signing up first. It also left sign-up open on purpose ("sign-up
  is allowed") without saying what that lets strangers do.
- "7b nonsense date: Blocked": only true for text that isn't shaped like a date (item 4).
- "5 login attempts a minute, 20 new records a minute": counted per area (item 5).

### What the first check got right (I re-tested these myself)

- Strangers can't see, change or delete any booking, profile or contact message. Ana
  can't see, change or delete Ben's things, can't book in his name, and can't make
  herself admin, either at sign-up or later. Customers can't use the admin export or
  delete accounts. Only the owner can.
- Harder tricks the first check didn't try were also blocked: searching other people's
  data through filters, handing a booking to someone else with special field tricks,
  changing your own email or "verified" status directly, changing a password without
  the old one, sending many requests in one batch, and reading `.env` or the server code
  through the website.
- Things customers type (dog name, address, notes) show as plain text on both the
  customer page and the admin page. Nothing ran in the browser.
- The contact form gives only short, plain error messages, even for broken or 3 MB
  requests. Deleting a customer also deletes their bookings. The owner can't delete
  their own account by mistake.
- The "Corrections to my first report" section above is accurate. I checked each claim
  against the original code in git.
- The "What you still have to do yourself" list still stands. Add: remove the key from
  `security-tests/attack.py`, and decide whether strangers should be able to sign up.

## Fixes after the second check, and re-test

Done 2026-10-30, as you asked: sign-up turned off, key out of the test script, impossible
and past dates refused, log-out button added. Then I re-ran every old attack plus the
new ones from the second check, on a fresh copy of the test data. Afterwards I stopped
the server and deleted the test data.

### What I changed

- **Sign-up is off** (new file `pb_migrations/1700000004_no_signup.js`). Strangers and
  customers can no longer create accounts. You add customers yourself in the PocketBase
  dashboard (`/_/`). The owner account can also add them through the server, but only
  as plain customers, never as another admin. This also closes the "who is a customer"
  leak (second check, item 3): sign-up attempts now just get "Failed to create record".
- **The mail key is out of the test script.** `security-tests/attack.py` now looks for
  anything *shaped* like a live key (`mk_live_` + letters/digits) instead of containing
  the key itself. A new test (4c) scans every project file for such a key.
- **Impossible and past dates are refused** (new file `pb_hooks/booking_date.js`, used
  by `pb_hooks/main.pb.js`). That covers new bookings and when a customer moves a
  booking. "Today" counts as fine. Old bookings can still have their notes edited after
  their date has passed. The customer sees "Booking not saved. Please check: date."
- **Log-out button** on the customer page (top right of "Book a walk"). It forgets the
  login in that browser, empties the form and the booking list, and stays logged out
  after a reload.
- The test script now also removes the bookings and contact messages it made. The
  browser test checks the log-out button and a booking for yesterday. README updated.

### Re-test results

`security-tests/attack.py`: **61 checks. All 49 attacks blocked, all 12 normal-use
checks OK.** That includes:
- all the original tests (1a to 9a)
- the new ones:
  - 3i: stranger signs up
  - 3j: sign-up reveals customers
  - 3k: Ana creates an account
  - 3l: owner creates a second admin
  - 4c: key in any project file
  - 7e/7h: impossible dates
  - 7f/7g: past dates
  - 6c: faked address vs. login limit
  - 8a to 8i: the harder tricks from the second check
- new normal-use checks:
  - F8: owner adds a customer
  - F9: booking for today works
  - F10: notes can still be edited
  - F11: the script cleaned up after itself

Browser check (`browser-test/run.py`, real Chrome): **18 of 18 passed**, no page errors.
The log-out check that failed before now passes. Screenshots are in
`browser-test/screenshots/`.

### What's left

Still open from the second check (not part of this round):
- **Logging out only works in that browser.** The login itself stays valid on the
  server for about 5 days. Someone who copied it (for example from a shared computer
  before log-out) can keep using it. Changing the customer's password ends it.
- **Rate limits are counted per area** (second check, item 5). It matters much less now
  that strangers can't sign up.
- **Dates far in the future are accepted** (up to the year 2099).
- **Not security:**
  - Password-reset emails can't be sent (no mail service set up).
  - Deleting customers and reading contact messages have no buttons on the admin page.
  - Accounts can still upload an unused profile picture. It's harmless, as checked.

Your jobs (still yours, code can't do them):
1. Cancel the old mail key and get a new one (see "What you still have to do yourself").
2. Stop using `walkies2026` anywhere.
3. Clean the old secrets out of git history before sharing or uploading the code.
4. Don't put the test accounts (password `password123`) on the live site. Remove them
   or change their passwords, and give the owner account a long, unique password.
5. Set a strong PocketBase dashboard password. You'll now use the dashboard to add
   customers.
6. Use HTTPS and set "Trusted proxy" when online.
7. `.env` still sits in the folder with the old key. Delete it (or blank the old key)
   once you have the new one, and never copy or upload it with the code.
8. Commit the changes. Nothing has been committed to git yet.
