The first item on an application security checklist is a second account, not a scanner: sign in as one customer and try to open another’s records. In 7 of the 21 third-party apps I audited, a confirmed authorization failure meant a logged-in user could read or write another customer’s data. Twelve checks follow, each with its test.
Those 21 apps are the ones I audited in June and July 2026, a selected set of audited apps and not a random sample, so the count tells you which check to run first, not how often AI-built apps fail it; the counts rest on human-verified findings. The list below is the testing layer of web app security for a single product: each check names the test that proves it, the result a correct build returns, and the evidence worth keeping.
The application security checklist
An application security checklist for one web app has 12 checks: tenant data isolated, every route authorized on the server, no private key in the browser or repository, hostile input rejected, the costly endpoint rate-limited, security headers sent, cross-origin access restricted, uploads bounded, vulnerable dependencies fixed, security events logged cleanly, the AI feature tested, a disclosure contact published.
It works as a web app security checklist for one product and as a security checklist for web application teams shipping a single codebase. Run the checks in this order, since the first three decide whether one customer can reach another’s data:
- One customer cannot read or change another tenant’s records.
- Every protected route and server action checks the caller’s role on the server.
- No private key sits in the browser bundle, the network traffic or the repository history.
- The server rejects input of the wrong type, the wrong length, or with fields it did not expect.
- The most expensive endpoint has a per-account limit.
- Production responses carry the security headers, and the app still works with them on.
- Cross-origin reads and cross-site state changes are refused.
- Uploads are checked for type and size, and stored files stay private.
- Dependencies with known vulnerabilities are fixed, and the app still passes its tests.
- Sign-ins and refused requests are logged, with no password, token or key in the line.
- The AI feature validates model output before it touches data and stops at a per-user spend limit.
- A security.txt file names a contact that actually receives reports.
Read down the first column of the table and you have an application security controls checklist; read across a row and you have the proof for one control. The test and expected-result cells are my method, and together they make a security testing checklist for the web app itself, run against the deployed application rather than the code on your laptop.
| Check | The test | Expected result | Evidence to keep | Goes deeper |
|---|---|---|---|---|
| 1. Tenant data isolated | Two ordinary accounts in two tenants; from one, request the other’s record ids through the app’s own API | Refused or empty | The two requests and responses, dated | multi-tenant data isolation |
| 2. Protected actions authorized on the server | Call each admin or owner-only route directly as an ordinary user, then signed out | Refused (401, 403, 404 or a redirect to sign-in) and nothing changed; under Supabase row-level security, an empty result with the targeted row read back intact | The route list with the status each returned | broken access control |
| 3. Private keys kept out of the browser and repo | Search the built JavaScript, the Chrome DevTools Network panel and the repository history for your providers’ key prefixes; decode every JWT found and read its role | Only publishable keys | The search terms and the empty results | which keys can live in the browser |
| 4. Hostile input rejected on the server | Send each write endpoint wrong types, over-long strings and unexpected fields, bypassing the form | A 4xx, or the unexpected fields dropped, and nothing invalid stored | The request set and the responses | input validation for a web app |
| 5. Costliest endpoint rate-limited | A burst of requests from one account, then ordinary use | Refused (a 429, for example) past the limit, normal service below it | The count at which refusals began | rate limiting in an API |
| 6. Security headers sent in production | Read the production response headers with curl -I, then click through the app | The headers present and no broken flow | The header dump | content security policy without unsafe-inline |
| 7. Cross-origin access restricted | A credentialed request with an origin you never approved; a state-changing form posted from another origin while signed in | The response does not name that origin in Access-Control-Allow-Origin; the change does not happen | Both responses | CSRF mitigation |
| 8. Uploads typed, sized and private | A disallowed type, an oversized file, a stored file’s plain URL opened signed out | All three refused | The three responses | file upload testing |
| 9. Vulnerable dependencies fixed | npm audit (by default it needs a lockfile) or the stack’s equivalent, then the app’s own tests | No unresolved high or critical advisory the app reaches; pip-audit’s README output shows no severity, so each advisory it lists is read against the app | The audit output, the fix, the test run | the npm audit command |
| 10. Security events logged without secrets | One sign-in with a test password you chose, one refused request; read the log lines they made | Both events logged; the test password, tokens and keys absent | The two log lines | how to prevent insufficient logging and monitoring |
| 11. The AI feature tested | Instruction-override inputs you wrote yourself; a spend test against the per-user limit | Model output checked before it touches data; the limit stops the spend | The inputs, the outputs, the limit hit | what prompt injection is |
| 12. Disclosure contact published | Fetch /.well-known/security.txt and send a test message to its Contact | The file parses and the message arrives | The file and the received message | a security.txt file example |
Two rows lean on a source. Check 2’s empty result comes from how Supabase describes a policy: “Think of a policy as adding a WHERE clause to every query”, and because “Matching no rows is not proof on its own”, the test reads the targeted row back afterward. For check 4, OWASP’s Input Validation Cheat Sheet says input validation “must be implemented on the server-side before any data is processed by an application’s functions”, and that “Allowlist validation is appropriate for all input fields provided by the user.”
To use this as a security checklist template in Excel, paste the table into a sheet and add a date column; there is no download. Saved as an xls file, the same sheet is the web application security checklist a customer’s reviewer can open without a browser.
Each check has a matching deliverable in the Production Hardening Sprint (check 3 has two; the mapping is mine). The sprint verifies 1.4 by running read and write tests as anonymous users, different roles, and separate tenants ; 1.3 by calling protected actions directly as unauthorized and underprivileged users and confirming rejection ; 2.1 by scanning source and built assets and inspecting browser traffic for private credentials , with 2.3 by checking runtime configuration, built assets, and each code path using administrative credentials ; 3.1 by submitting invalid and hostile payloads and confirming safe rejection or handling ; 3.6 by simulating bursts and verifying enforcement without blocking ordinary use ; 3.4 by inspecting response headers and exercising the application to confirm intended protections without broken flows ; 3.3 by testing approved and unapproved origins, credential handling, and state-changing request protections ; 3.2 by testing disallowed formats, oversized files, unauthorized downloads, and storage listings ; 3.5 by recording the dependency scan, remediation, and regression checks after changes ; 8.1 by tracing a test request across services and checking log content for sensitive fields ; 3.11 by running injection test cases against each feature, a spend simulation that hits the limit, and a provider-outage simulation in staging ; and 3.9 by checking the published file, contact details, and delivery of a test message.
How to fill it in
This is a manual security testing checklist: a person runs it, in the order above, against the deployed app. A frontend security checklist before launch covers checks 3, 6 and 7 of it and stops there, since those three can be seen from the browser; the other nine need the server or the data. Keep your code security checklist notes in the ledger’s result column, one line per check. Read as fixes instead of tests, the same twelve lines make a web application hardening checklist. A scanner can run checks 6 and 9 and part of 3; the rest need a person, and checks 1 and 2 need two accounts (my reading).
Identity: checks 1 and 2
Testing as the admin teaches you nothing here, because an admin can open everything and every request succeeds. The server side permission checks checklist is short to write: list every route and server action, then call each one directly as the wrong role, and once more signed out. For check 1, sign in to the first tenant’s ordinary account, take a record id that belongs to the second tenant, and ask for it through the app’s own API. The two-account method, step by step, is in the API security checklist. File the requests, the responses and the date.
Secrets: check 3
Searching the source and skipping the built bundle is where this check goes wrong: the bundle is what the browser downloads, with every environment value the build baked in. Open the deployed app in Chrome, press Command+Option+J (Control+Shift+J on Windows and Linux), click the Network tab and reload. Then use the Search tab, which Chrome describes as the place “to search the HTTP headers and responses of all resources for a certain string or regular expression.” Search there for your providers’ secret-key prefixes, and run the same search over the repository’s history. On Supabase, the legacy anon and service_role keys are both a “JWT (long-lived)”, one “Legacy version of publishable keys” and the other “Legacy version of secret keys”, so decode each JWT you find and read its role; a prefix search alone cannot tell the public one from the secret one (my reading). File the search terms and the empty results. The builder-side fix is a set of prompts that change what the builder writes.
Input and abuse: checks 4 and 5
Validation that lives only in the form protects nothing on the server, because the endpoint behind the form sees whatever request arrives. Replay each write request outside the form, with curl or an API client, and change one thing at a time: a number where text belongs, a string far past any sensible length, a field the form never sends. Then check the table for anything that got stored. For check 5, send a quick burst from one signed-in account at the endpoint that costs you the most to serve, then use the app normally and confirm it still answers. File the request set, the responses and the count at which refusals began. The API security checklist above covers the API-shaped version of both checks.
The browser boundary: checks 6, 7 and 8
A content security policy that breaks the app gets switched off in a hurry, and then the headers check fails quietly. Read the headers from a real production response with curl -I against your live domain, look for a content security policy, HSTS and a framing rule, then click through sign-in, payment and any upload with them on. For check 7, send a credentialed request carrying an origin you never approved: the response must not name it in Access-Control-Allow-Origin, since a credentialed response needs an explicit origin and, without the credentials header, “the response is ignored by the browser and not returned to web content.” Then post a state-changing form from a page on another origin while signed in, and confirm nothing changed. For check 8, a file in a Supabase private bucket is “not accessible via a public URL”, while a signed URL is “time-limited” and works until it expires, so open the plain path signed out, not a signed link the app handed you. Before that, upload one file of a type the app should refuse and one far over its size limit, and confirm both are rejected with nothing stored.
In one app I audited, a medical app’s API, whenever its environment setting said development (the default when unset), allowed requests from any website and allowed credentials, with every method and header. The report notes the impact was limited because the token lived in browser storage, not a cookie, and that adding any cookie session would make it a cross-site data leak. The lesson I take from it: a cross-origin rule is only as strict as the environment the server thinks it is in, which is why the cross-origin check reads the headers production actually sends.
Supply chain: check 9
Running npm audit fix and shipping without a test run swaps one risk for another. npm describes the command as one that “submits a description of the dependencies configured in your project to your default registry and asks for a report of known vulnerabilities”, and its audit-level setting accepts the severities info, low, moderate, high and critical. “By default npm requires a package-lock or shrinkwrap in order to run the audit.” With fix, “remediations will be applied to the package tree”; add --force and it may “install modules outside your stated dependency range (including SemVer-major changes)”, which is why the regression run follows every fix. On Python, pip-audit’s README output shows name, version, ID and fix versions, with no severity column. File the audit output, the change and the test run.
Visibility: check 10
Logging the whole request body puts the password from every sign-in into your logs. Sign in once with a test password you chose for this, send one request the app should refuse, then read the log lines the two made. Search the logs for that test password and for your session token: finding both events and neither secret is the pass. A search for a value nobody typed proves nothing, which is why the password has to be one you entered. File the two log lines.
The AI feature and disclosure: checks 11 and 12
An AI feature with no per-user spend ceiling lets one account run up the bill. Write a few inputs of your own that tell the model to ignore its instructions, and confirm the output is validated before it writes anything; keep the tests on your own app, with your own inputs, never attack strings copied from elsewhere. Then drive one test account to the app’s own per-user limit and confirm the next call is refused. For check 12, RFC 9116 puts the file under /.well-known/ over HTTPS and requires two fields: Contact, “a method that researchers should use for reporting security vulnerabilities”, and Expires, which it recommends keeping “less than a year into the future to avoid staleness.” Fetch the file, send a test message to its Contact, and file the message you received.
The same list in other vocabularies: OWASP, NIST and Django
Other checklists cover the same ground in other words. OWASP’s Web Security Testing Guide is a testing resource for web application developers and security professionals, NIST SP 800-53 catalogs security and privacy controls for information systems and organizations, and Django’s deployment checklist covers one framework. An audit checklist is this list with a date, a name and a signature.
For a web application security checklist, OWASP’s own answer is the OWASP Web Security Testing Guide, and its project page offers v4.2 “as a web-hosted release and PDF” while stating “We are currently developing release version 5.0.” The checklist spreadsheet is not on that page. It sits in OWASP’s own wstg repository on GitHub, in a checklists folder that holds Excel, Markdown and JSON versions, and the folder’s README says “The current (Excel) checklist is based on v4.2 of the OWASP Testing Guide, as content for other versions is still under development.” That file, the OWASP testing checklist XLSX, is the one I would start from before any other GitHub copy or OWASP Top 10 checklist. Testing each of the Top 10 risks is a separate job: how to test the OWASP Top 10 vulnerabilities, one category at a time.
A NIST SaaS security checklist points at NIST SP 800-53, “Security and Privacy Controls for Information Systems and Organizations”, which NIST describes as “a catalog of security and privacy controls for information systems and organizations.” Its controls are “implemented as part of an organization-wide process to manage risk”, so it is written for organizations and is not a per-app test list (my reading).
A Django checklist is Django’s deployment checklist, which opens with “The internet is a hostile environment.” and says some of its checks “can be automated” with manage.py check --deploy, run “against your production settings file”. In my mapping, its SECRET_KEY setting belongs to check 3, CSRF_COOKIE_SECURE and SESSION_COOKIE_SECURE to checks 6 and 7, and LOGGING to check 10.
Website security standards, for one web app, come down to these bodies: OWASP for how to test, NIST for which controls an organization keeps. Add a date, a name and a signature column and the list becomes a website security audit checklist that a reviewer of the web application can sign; print that page to PDF and it is your security audit checklist PDF. An application security assessment checklist is the same list run by someone outside your team, which is a web application security audit.
A filled example
The app here is constructed, not a real one. Its assumptions: Next.js on Vercel, Supabase with row-level security, Stripe Checkout, one AI feature, built with a builder tool. The middle column shows what a filled ledger row looks like; it is never a claimed result for a real app. The right-hand column holds what my audits measured, where they measured that check.
| Check | Ledger row for the example app (test run, result, evidence) | What my audits measured |
|---|---|---|
| 1. Tenant data isolated | Second tenant’s account asked the Supabase API for the first tenant’s project ids; empty result, the first tenant’s rows read back intact; both request and response pairs saved | 7 of the 21 third-party apps: the cross-user or cross-tenant failures described under the opener |
| 2. Protected actions authorized | Every admin route handler called as an ordinary user and signed out; each refused or redirected to sign-in; route list with statuses saved | 11 of the 21 third-party apps had unauthenticated endpoints doing privileged work |
| 3. No private key in the browser | Network panel search for the Stripe and Supabase secret-key prefixes found nothing; the JWT in the bundle decoded to the anon role; search terms saved | 6 of the 21 third-party apps shipped a real secret |
| 4. Hostile input rejected | Project-create endpoint sent a number as the name, an over-long name and an extra owner field; the first two rejected with a validation error, the extra field dropped, nothing stored | Not measured in my audits |
| 5. Costliest endpoint rate-limited | Burst at the AI endpoint from one account; refusals began at the limit set in code; normal use worked afterward | 13 of the 21 third-party apps had no rate limiting on their most expensive endpoint |
| 6. Security headers sent | curl -I on the production domain showed the content security policy, HSTS and framing rule; sign-in, checkout and upload clicked through | Not measured in my audits |
| 7. Cross-origin access restricted | Credentialed request from an unapproved origin got no Access-Control-Allow-Origin for it; a form posted from another origin changed nothing | Not measured in my audits |
| 8. Uploads typed, sized and private | Disallowed type, oversized file and a private file’s plain URL opened signed out; all refused | Not measured in my audits |
| 9. Vulnerable dependencies fixed | npm audit on the lockfile, high advisories fixed, test suite rerun and passing; output saved | Not measured in my audits |
| 10. Events logged without secrets | Sign-in with a chosen test password and one refused request; both events logged, test password and token not found | Not measured in my audits |
| 11. AI feature tested | Own instruction-override inputs; model output validated before any write; the test account refused at its per-user limit | 8 of the 14 AI apps had a live prompt-injection path, with untrusted text flowing straight into the model’s instructions |
| 12. Disclosure contact published | /.well-known/security.txt fetched with Contact and Expires present; test message received | Not measured in my audits |
The right-hand counts come from my June and July 2026 audits: 21 third-party apps, and 14 AI apps for check 11. They are a chosen group of audited apps rather than a sample of the market, so read each count as a reason to run that check early, never as a failure rate for your app or for AI-built apps at large. A 7 point security checklist for SaaS apps, in my cut, is checks 1 to 5, 9 and 10; the complete publish security checklist is all twelve.
How to verify the result
A check is done when its evidence row exists: the date, who ran it, the command or click path, the result, and a screenshot or log line. Twelve rows make a ledger a reviewer can read; a scanner report alone covers only the checks a scanner can run.
| Ledger column | What goes in it |
|---|---|
| Check | The check’s number and name from the list |
| Date | The day the test ran |
| Who ran it | The person, by name or role |
| Command or click path | The exact command, request or DevTools path used |
| Result | What came back, in one line |
| Evidence file | The saved response, log line or screenshot |
A check without a row is not done, however sure anyone feels about it. Where the ledger sits among an app’s other documents is under keep a test map and an evidence ledger. What a builder’s own security scan covers, and what stays with you, is the subject of the vibe code security check. For an app built with an AI builder, the vibe coding security checklist orders its checks by when each risk arrives.
Where the sprint does this
Deliverable 13.1 of the Production Hardening Sprint, the production readiness report, is verified this way: account for all 123 IDs; keep failures visible until resolved and explain genuine non-applicable items. The twelve checks’ deliverables, listed in the paragraph after the checklist table, are among those IDs. Formal third-party certifications and independent audit opinions are separate from these engineering deliverables. Each ID, with its verify line, is listed on the published scope.
Common questions about security checklists for web apps
What is a security checklist?
A security checklist is a list of conditions an app has to meet, each paired with a test that proves it and the evidence that it passed. A list of controls with no test is a statement of intent; the test and the saved result are what let someone else check your work.
What are the top 10 web security checklists according to OWASP?
The OWASP Top 10 is not ten checklists: OWASP calls it “a standard awareness document for developers and web application security”, covering “the most critical security risks to web applications.” Two of OWASP’s checklist-shaped projects are the Web Security Testing Guide, whose Excel checklist is based on v4.2, and the Secure Coding Practices Quick-reference Guide, which OWASP says “has now been archived”, its checklists “migrated to the Developer Guide”.
Where can I find the OWASP Wstg checklist?
The OWASP WSTG checklist is in OWASP’s wstg repository on GitHub, in the checklists folder, as an Excel file based on v4.2 of the guide. The project page itself offers the guide as a web-hosted release and a PDF.
How to do security testing for web applications?
Run the twelve checks above in order against the deployed app, with two ordinary accounts for the first two, and write each result into the ledger. Then add a scanner for what a scanner can run: the headers, the dependencies and part of the key search.
How to make an audit checklist?
Start from a list of checks that each have a test, then add three columns: the date, the name of whoever ran it, and a signature. Fill one row per check as it runs, and keep the evidence file next to each row.
The checks in this guide show you where the app is open. The sprint below closes those gaps, tests the result and writes the evidence down.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase