# Security & production test report – AMZ Shift Picker SaaS

Date: 28 Sep 2026 · Server 1.0.0 · Extension 4.0.0

## How it was tested
- **Automated (run here):** `saas` – 42 tests on a real MySQL server with the real app over HTTP
  (Stripe's network calls replaced by a fake; webhook signatures produced and checked by the real
  Stripe library). `amazon-shift-bot` – 27 tests, including the real `background.js`/`license.js`
  against signed tickets.
- **Not testable from the development PC – must be done by you (README steps 6–9):**
  real Stripe test-mode checkout and webhook delivery, real email delivery through cPanel,
  the extension in real Chrome (install, restart, update, reinstall), deployment on cPanel,
  and a backup restore.

## Attack tests
| # | Test | Attack | Expected | Actual | Result |
|---|---|---|---|---|---|
| 1 | SQL injection | `' OR '1'='1`, `'; DROP TABLE users; --` in login (web + API) and admin search | Treated as text; login fails; tables intact | 401 / normal page; tables intact | PASS |
| 2 | XSS | `<script>`/`<img onerror>` as name at registration; same text written into DB | Refused at input; escaped on /account and admin pages; CSP without `unsafe-inline` | 400 on register; `&lt;script&gt;` in output; CSP `script-src 'self'` | PASS |
| 3 | CSRF | POST /checkout without token; with token but `Origin: https://evil.example` | 403 | 403 / 403 (valid request 303) | PASS |
| 4 | IDOR/BOLA | User A opens B's invoice `/account/invoices/<B>`, `/api/users/<B>`, `/api/payments/<B>`, `?user_id=B` | 404 / own data only | 404, 404, 404, A's own data | PASS |
| 5 | Admin authorization | Normal user (cookie **and** extension token) → `/api/admin/users`, `/admin/*`, POST extend | 403, nothing changed | 403 everywhere; no extension row | PASS |
| 6 | Authentication bypass | Missing/garbage/expired bearer tokens; forged admin cookie; use after logout | 401/403 | 401 / 403 / 401 | PASS |
| 7 | Brute force | 10 wrong passwords, then the right one | Account locked 15 min | Locked, generic message | PASS |
| 8 | Rate-limit bypass | Changing IP per request against one email | Per-email limit still applies | 429 on 16th try | PASS |
| 9 | License brute force | 11 guesses in an hour | 429 | 10× 400, then 429 | PASS |
| 10 | Duplicate activation | Same key twice, same account | 2nd: "License already activated" | 409 | PASS |
| 11 | Concurrent activation | 10 simultaneous activations of one key; 2 accounts at once | Exactly 1 succeeds | 1× 200 + 9× 409; owner 200, other 400 | PASS |
| 12 | Key reuse by another account | B activates A's used **and** unused key | Refused | 400, A's key untouched | PASS |
| 13 | Account deletion/recreation | A activates, A closed, same email registers again (new account) | Refused; old user can't be hard-deleted | 400; DELETE blocked by foreign key | PASS |
| 14 | Parameter tampering | `plan=PLAN_99`; extra `price`, `amount`, `duration`, `price_id` fields | Unknown plan refused; extras ignored | 400; Stripe got `price_test_15` | PASS |
| 15 | Modified plan in payment | Event metadata says PLAN_30 on a PLAN_15 checkout | 15-day license (plan from DB) | PLAN_15 / 15 days | PASS |
| 16 | Modified price | PLAN_30 checkout completed with £35 | No license, logged `payment_mismatch` | No license, logged | PASS |
| 17 | Modified duration | Extension sends `duration: 365, plan: PLAN_30` with activation | Ignored | Expiry = activation + 15 days | PASS |
| 18 | Fake payment request | `/checkout/success?payment_success=true` without webhook | No license | 0 licenses | PASS |
| 19 | Fake Stripe webhook | Wrong secret; bogus signature; no signature | 400, nothing stored | 400 ×3, nothing stored | PASS |
| 20 | Replayed webhook | Correctly signed but 1 hour old | 400 | 400 | PASS |
| 21 | Duplicate webhook | Same event 5× in parallel + a 2nd event type for the same payment | One payment/subscription/license/email | Exactly one of each | PASS |
| 22 | Local clock manipulation | Client `Date`/`x-client-time` headers; PC clock set back vs. ticket | Server time only; extension locks on clock-back | Server says expired; extension state `clock` | PASS |
| 23 | Edited local storage | Ticket with longer `exp`; ticket signed by another key; old offline license object | Refused | Signature fails → locked | PASS |
| 24 | Expired subscription bypass | Expired license, then status/tickets | Locked | `expired` | PASS |
| 25 | Refund | `charge.refunded` for a paid key | License revoked, activation refused | Revoked; 410 | PASS |
| 26 | Admin revoke / suspend | Revoke license; suspend account | Extension loses access; sessions killed | Status `revoked`; token 401; login 403 | PASS |
| 27 | Password reset | Reset link reused; old sessions | One-time; all sessions (web + extension) ended | 400 on reuse; 401 / redirect | PASS |
| 28 | Account enumeration | Register/forgot with existing vs. new email | Same response | Identical pages; owner gets an email | PASS |
| 29 | Admin 2FA | Wrong code; replayed code | Refused | 401 / 401 | PASS |
| 30 | Admin session timeout | Admin session idle > 30 min | Logged out | Redirect to login | PASS |
| 31 | Error leakage | Malformed JSON; 20 KB body | Generic message, no stack; 413 | 400 generic; 413 | PASS |
| 32 | Security headers | GET / | CSP, nosniff, frame-ancestors none, no X-Powered-By, no CORS | All present | PASS |
| 33 | Secrets in extension | Scan package for `sk_`, `whsec_`, private keys, localhost | None | None (public ticket key only) | PASS |

## Final audit checklist
| Item | Status |
|---|---|
| Extension is Manifest V3 | ✅ |
| Existing bot works | ✅ unit tests (matching, one-at-a-time, auto-apply logic) · ⏳ manual check in Chrome |
| Admin panel / admin auth / 2FA / timeout | ✅ |
| Normal users can't access admin APIs | ✅ |
| Registration, email verification, login, password reset | ✅ (email content captured in tests) · ⏳ real cPanel SMTP |
| £35 and £55 plans, Checkout, webhook signature, duplicates | ✅ with fake Stripe network · ⏳ real Stripe test mode |
| Payment, invoice ID, subscription, license, license email | ✅ |
| License once only / not reusable / plan can't be changed / server time | ✅ |
| Admin sees user, payment, invoice, subscription, license; suspend; revoke; logged | ✅ |
| Rate limiting, IDOR, XSS, SQLi, auth bypass, Stripe spoofing, race condition | ✅ |
| No secrets in extension | ✅ |
| No secrets in Git | ✅ `.gitignore` excludes `.env*`; this folder is not a Git repository yet, so there is no history to leak |
| Production/staging/development separated | ✅ config refuses test Stripe keys in production unless explicitly allowed · ⏳ you create staging |
| Backups configured and restore tested | ⏳ on your cPanel (README step 8) |
| Privacy policy, terms, refund policy | ✅ pages exist · ⏳ have them reviewed |
| Web Store metadata, unlisted distribution | ✅ `amazon-shift-bot/store/LISTING.md` · ⏳ screenshots + submission by you |
| Production build tested | ✅ packages built · ⏳ install from the store after approval |

**Not "production ready" until the ⏳ items are done.**

## Remaining risks / limitations
1. **The bot runs in the user's browser.** The server can stop key sharing, reuse, plan changes
   and clock tricks, and tickets can't be forged. A technically skilled user who copies the
   extension's code and deletes the checks can still run their own copy offline. No browser
   extension can fully prevent that.
2. **Amazon terms / store policy.** Automating applications on Amazon's hiring site may break
   Amazon's terms. Google may reject or remove the item (unlisted items are reviewed too), and
   Stripe may review the business. Get advice before you launch.
3. **`<all_urls>` content script** (`guide.js`) is needed for Amazon's application pages on other
   addresses. It makes Chrome's review stricter and shows a broad permission warning. If you can
   list the exact Amazon addresses used, it can be narrowed.
4. **Session token in `chrome.storage.local`.** Web pages can't read it. The extension's own
   scripts can. Signing out, a password reset or a suspension revokes it on the server.
5. **Offline grace:** after a revoke/suspension the bot stops within 5 minutes online. If the
   PC is offline it stops when its ticket ends (at most 6 hours).
6. **Legal pages are templates.** Have them checked for your business (company details, UK
   consumer rules).
7. **Old signing key:** `New Bot\keygen.html` still contains the old private key in a
   OneDrive-synced folder. v4 no longer trusts it. Delete that file once all customers are on v4.
8. Refunds and chargebacks are handled from Stripe's dashboard. The app reacts to
   `charge.refunded`. Disputes/chargebacks are not handled automatically: revoke the license by
   hand in Admin.
