OWASP Top 10 Checklist: Review and Test Plan for Your Own App
CybersecurityLesson 4 of 45 min read
What you'll learn
- Convert the OWASP Top 10 themes into a short, answerable review checklist
- Write an access-control matrix and automate it as a test
- Plan which checks run in code review, in CI and in periodic testing
- Prioritise fixes by exposure and impact so the work gets done
On this page
A freelance developer has just finished an invoice-sharing app for a client. It works, the tests are green, and then the client asks one sentence: "Is it secure?" That question has no yes or no answer, but it has a good response: "Here is what I checked, here is how, and here is what I would fix first." This lesson builds that response. The OWASP Top 10 (current edition) is not a compliance standard, but it is an excellent skeleton for a structured review of an app you own.
Step one: describe the app on half a page
A checklist is only useful when applied to something specific. Before reviewing, write down the data the app holds, who the user roles are, which endpoints accept input, which external services it calls and where it runs. Anything left out of this description will be left out of the review. A few minutes spent here prevents the most common review failure, which is carefully checking the parts you already remembered.
Step two: one checklist, grouped by theme
ACCESS CONTROL
[ ] Every endpoint that takes an id checks ownership or role on the server.
[ ] Admin routes are denied by default and tested with a normal user.
[ ] Role and permission fields cannot be set from request bodies.
INJECTION AND OUTPUT
[ ] All SQL uses parameters or a query builder; no string-built queries.
[ ] No user input reaches a shell, template engine or file path unchecked.
[ ] User-supplied text is encoded for the page context where it appears.
AUTHENTICATION AND SESSIONS
[ ] Login attempts are throttled; admin accounts require MFA.
[ ] Passwords use a slow salted hash; reset tokens are random and expire.
[ ] Session cookies are HttpOnly, Secure and SameSite; new id after login.
CRYPTOGRAPHY AND DATA
[ ] HTTPS everywhere; no secrets in source control; keys rotated.
[ ] Sensitive fields are encrypted or not stored at all.
[ ] Backups are protected as strongly as production data.
CONFIGURATION
[ ] Debug off in production; generic error pages; no default accounts.
[ ] Security headers set; storage buckets and admin panels not public.
[ ] Staging uses synthetic data.
COMPONENTS
[ ] Dependency scan runs in CI; owner named for updates.
[ ] Unused packages and endpoints removed.
DESIGN
[ ] Abuse cases written for each risky feature (limits, resets, coupons).
[ ] Critical state changes are atomic and enforced by the database.
OUTBOUND REQUESTS (SSRF)
[ ] Any feature that fetches a user-supplied URL has scheme and
destination limits, timeouts and redirect handling.
LOGGING AND RESPONSE
[ ] Logins, denials and admin actions are logged without secrets.
[ ] At least three alerts exist and have been tested.
[ ] There is a one-page incident plan and a tested backup restore.Each line is a yes or no question. A "no" or "I do not know" becomes a ticket. Resist the urge to add fifty more items: a short list that gets completed beats a long one that gets skimmed.
Step three: automate the access-control matrix
Of everything on the list, access control is the area where automated tests pay back most, because the failure is invisible to a single-user tester. The idea is a matrix: rows are users and roles, columns are resources, cells say whether access is expected. A test walks the matrix and fails on any difference. The version below uses SQLite so you can run it, but the same table can drive HTTP calls against a test server.
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE invoices (id INTEGER PRIMARY KEY, owner TEXT, total INTEGER);
INSERT INTO invoices VALUES (1, 'asha', 120), (2, 'bruno', 80);
""")
def get_invoice(user: str, role: str, invoice_id: int):
if role == "auditor":
return db.execute("SELECT id, owner, total FROM invoices WHERE id = ?", (invoice_id,)).fetchone()
return db.execute(
"SELECT id, owner, total FROM invoices WHERE id = ? AND owner = ?", (invoice_id, user)
).fetchone()
# (user, role, invoice id, should the caller see it?)
MATRIX = [
("asha", "customer", 1, True),
("asha", "customer", 2, False),
("bruno", "customer", 1, False),
("carol", "auditor", 1, True),
("carol", "auditor", 2, True),
]
failures = 0
for user, role, invoice_id, expected in MATRIX:
allowed = get_invoice(user, role, invoice_id) is not None
status = "ok " if allowed == expected else "FAIL"
failures += status == "FAIL"
print(f"{status} {user}/{role} -> invoice {invoice_id}: visible={allowed}")
print("failures:", failures)Output
ok asha/customer -> invoice 1: visible=True ok asha/customer -> invoice 2: visible=False ok bruno/customer -> invoice 1: visible=False ok carol/auditor -> invoice 1: visible=True ok carol/auditor -> invoice 2: visible=True failures: 0
Put the matrix next to your normal tests and run it in CI. When someone adds a new endpoint or role, adding a row becomes part of the definition of done. The negative rows (expected False) are the valuable ones, since they are the cases a happy-path test never touches.
Step four: decide where each check runs
| When | What to run | Who |
|---|---|---|
| Code review | Checklist questions for the changed area; new endpoint means new matrix row | Author and reviewer |
| Every build (CI) | Dependency scan, secret scanning, linting for dangerous functions, matrix tests | Pipeline |
| Before release | Header and configuration audit, a manual pass over login, reset and file upload | Developer |
| Periodically | Restore a backup, rehearse the incident plan, review alerts, consider an external penetration test | Team and specialists |
Step five: prioritise by exposure and impact
Not every unchecked box deserves a sprint. Sort findings by two questions: how easily can it be reached (an anonymous internet user, a logged-in user, an admin only) and what is lost if it is (every customer's data, one user's profile, a cosmetic page). A missing ownership check on an endpoint that returns invoices outranks a missing referrer header every time. Write each finding as a one-line sentence with a proposed fix and an owner, and handle the top three before anything else. Tie authentication-related items back to the way your app issues tokens, covered in JWT authentication.
Common mistakes
- Running a scanner and calling it a review. Symptom: a clean report on an app where user A can open user B's invoice, because scanners cannot know your business rules. Fix: pair tools with the access matrix and manual checks.
- Testing with one admin account. Symptom: every test passes, and the first normal customer to explore finds a data leak. Fix: at least two normal users and one limited role in every test.
- Treating the checklist as a one-off. Symptom: it was clean at launch and stale six months later. Fix: run it per release or at least quarterly.
- Fixing symptoms in order of discovery. Symptom: low-risk header findings fixed while the unauthenticated endpoint stays open. Fix: sort by exposure and impact.
- Skipping the report-back. Symptom: the client or manager hears nothing and assumes either everything or nothing is fine. Fix: a one-page summary of what was checked, what was found and what is next.
How this shows up in real projects and interviews
Teams that do this well keep the checklist in the repository, wire the cheap parts into CI and review the rest at release time. In interviews, being able to describe a concrete process ("I keep an access matrix as a test and a short checklist per release") reads as far more credible than reciting a list of categories from memory. If you want to turn this skill into a career move, look at security-related roles and the skills employers ask for, and practise explaining your process out loud with the tips in thinking out loud in interviews.
Try it yourself
Apply the checklist to a project you own. Write the half-page description, answer each line with yes, no or unknown, create a matrix of at least two users and two roles with five or more rows (including negative ones), and list your top three fixes with owners.
Show a hint
Count unknown as no for planning. If you have only one user role, add a second account of the same role, since user-to-user access is the most common gap.
Key takeaways
- A review starts with a half-page description of the app: data, roles, inputs, integrations, hosting.
- Group the OWASP Top 10 themes into one short yes-or-no checklist and turn every no into a ticket.
- Automate an access-control matrix with negative cases; it catches what single-user testing cannot.
- Spread checks across code review, CI, pre-release and periodic exercises.
- Prioritise by how reachable a flaw is and how much it exposes, and finish the top few first.
Test yourself
A trick from this lesson's ride: A checklist covers only what you remembered to write down: describe the app first. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
How do I use the OWASP Top 10 to test my own application?
Describe the app, turn each risk theme into yes or no questions, test the answers with a mix of code review, automated scanning and manual checks, and rank the findings by how exposed they are and how much they would expose. Re-run it every release.
Is the OWASP Top 10 a security standard I can be certified against?
No. It is an awareness document that lists the most critical categories of web application risk. For a more detailed, testable set of requirements, OWASP also publishes the Application Security Verification Standard, which teams use for deeper assessments.
Which OWASP Top 10 risks should a small team fix first?
Start with access control, injection, authentication and exposed secrets, since they are both common and high impact. Then handle dependency updates and configuration, and add logging and alerts so problems are noticed.