Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- A website security audit does not need to be expensive or time-consuming if you follow a structured process. This step-by-step guide walks through everything from scope definition to remediation tracking, with the right tools for each phase.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
How to Run a Website Security Audit: Step-by-Step Process
A website security audit is a structured process for identifying vulnerabilities in a web application before attackers do. For most development teams, audits happen reactively — after an incident, or when a customer asks for a security questionnaire.
The better approach is a repeatable, scheduled audit process that you run every quarter. This guide gives you exactly that: a 7-step process with specific tools for each phase, designed for developers and small teams who do not have a dedicated security team.
Who Should Run This Audit
This process is designed for: - Developers doing a pre-launch security review of a new application - SaaS founders preparing for SOC 2 or enterprise customer security reviews - Engineering teams adding a quarterly security review to their process - Anyone who has just been asked "do you have a security audit?"
It is not a penetration test. It will not find every vulnerability. What it will find is the entire class of common, exploitable, and often embarrassing vulnerabilities that comprise the majority of real-world web application compromises.
Step 1: Define Scope
Before running any tools, define what you are auditing. This prevents scope creep, focuses your effort, and creates a document trail.
Scope definition checklist
- Domains and subdomains: List every domain and subdomain in scope (yourdomain.com, app.yourdomain.com, api.yourdomain.com, admin.yourdomain.com)
- Authentication states: Unauthenticated, authenticated as a regular user, authenticated as an admin
- Excluded systems: Third-party services you do not control (Stripe, Auth0, Cloudflare) — note them as out of scope but document that they are used
- Data classification: Identify which parts of the application handle PII, payment data, or credentials
- Test environment: Prefer running tests against a staging environment, especially for active testing
Write this down. A one-page scope document is enough and becomes part of your audit report.
Step 2: Automated Scanning
Automated scanning is the most efficient way to surface common vulnerabilities. Run it first — it establishes your baseline and often finds 10–15 issues before you have looked at a single line of code.
Surface-level scan (infrastructure and headers)
Run a free scan at ZeriFlow. In 60 seconds you get: - TLS/HTTPS configuration grade - All HTTP security headers checked (CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP, X-Content-Type-Options) - Cookie security flags for every cookie - DNS security configuration - Email authentication (SPF, DKIM, DMARC) - A /100 security score with prioritized findings
This single scan covers the entire infrastructure and transport layer and gives you a prioritized list of fixes.
DAST (Dynamic Application Security Testing)
For a deeper automated scan that tests the running application, use:
- OWASP ZAP (free) — automated scanner that crawls your site and tests for XSS, SQL injection, CSRF, and more
- Burp Suite Community Edition (free) — intercepting proxy for manual and automated testing
Running OWASP ZAP automated scan:
# Run ZAP in Docker — quick automated scan
docker run -t owasp/zap2docker-stable zap-baseline.py \\
-t https://yourapp.com \\
-r zap-report.htmlSSL/TLS deep scan
# Check TLS configuration details
# Or use the web interface at ssllabs.com/ssltestRun your domain through Qualys SSL Labs for a detailed TLS grade. Check for deprecated protocols (TLS 1.0, 1.1), weak ciphers, and certificate chain issues.
Step 3: Manual Security Checks
Automated tools have blind spots. Manual checks cover authentication logic, authorization enforcement, and application-specific vulnerabilities.
Authentication
- [ ] Test password reset flow: does it use time-limited tokens? Does it invalidate old tokens?
- [ ] Test account lockout: is there brute force protection on the login endpoint?
- [ ] Check for username enumeration: does the app respond differently to valid vs invalid usernames?
- [ ] Test session invalidation on logout: is the server-side session actually destroyed?
- [ ] Check for concurrent session handling: can the same account be logged in from multiple sessions?
Authorization
- [ ] Test Insecure Direct Object References (IDOR): can you access
/api/users/123when you are user 456? - [ ] Test privilege escalation: can a regular user perform admin actions by modifying request parameters?
- [ ] Check API endpoints for missing authentication middleware: try hitting API routes without a token
# Quick IDOR test
curl -H "Authorization: Bearer [user-A-token]" \\
https://api.yourdomain.com/api/invoices/[user-B-invoice-id]
# Should return 403 Forbidden, not the invoice dataInput validation
- [ ] Test for reflected XSS in search fields and URL parameters
- [ ] Test for stored XSS in user profile fields, comments, or any user-controlled content
- [ ] Check file upload handling: can you upload a PHP or HTML file? Are file types validated server-side?
- [ ] Test for open redirects in
?redirect=,?return=, or?url=parameters
Security headers (manual verification)
curl -I https://yourdomain.com 2>/dev/null | grep -i \\
-e "content-security-policy" \\
-e "strict-transport-security" \\
-e "x-frame-options" \\
-e "x-content-type-options" \\
-e "referrer-policy" \\
-e "permissions-policy"Step 4: Code Review
Manual code review focuses on patterns that automated scanners miss because they require understanding context.
What to look for
Authentication and session management:
// Look for session tokens that are too short or predictable
const sessionId = Math.random().toString(36); // WRONG — predictable
const sessionId = crypto.randomBytes(32).toString("hex"); // CorrectSQL injection risk:
// Look for string interpolation in database queries
const query = `SELECT * FROM users WHERE id = ${userId}`; // WRONG — SQL injection risk
const query = "SELECT * FROM users WHERE id = $1"; // Correct — parameterizedSensitive data in logs:
// Look for credentials or tokens being logged
console.log("Login attempt:", email, password); // WRONG — logs passwords
console.log("Login attempt for:", email); // CorrectHardcoded secrets:
// Look for these patterns
const apiKey = "sk-live-abc123"; // Hardcoded secret
const stripeKey = process.env.STRIPE_SECRET_KEY; // CorrectCode review tools
- Semgrep (free, open source) — static analysis with security-focused rules
- ESLint with security plugins — eslint-plugin-security, eslint-plugin-no-secrets
# Run semgrep with the security ruleset
semgrep --config=p/security-audit --output=semgrep-report.json .Step 5: Dependency Audit
Third-party dependencies are a major attack vector. The Log4Shell vulnerability in 2021 and the event-stream incident are examples of how a single dependency compromise can affect thousands of applications.
Node.js
npm audit --json > audit-report.json
# Get a summary
npm audit 2>&1 | tail -5Python
pip install pip-audit
pip-audit --output=json > audit-report.jsonRuby
gem install bundler-audit
bundle-audit check --updatePHP (Composer)
composer auditPrioritizing dependency findings
| Severity | Action |
|---|---|
| Critical | Fix immediately — check if you are using the vulnerable function/feature |
| High | Fix within 48 hours |
| Moderate | Fix in next sprint |
| Low | Fix when convenient, or accept risk with documentation |
Important: Not every npm audit finding is an active risk in your application. A high-severity vulnerability in a server-side CLI tool that never processes user input may not be exploitable in your context. Review the advisory to understand the attack vector.
Step 6: Reporting
A security audit is only as useful as the action it generates. Create a structured report with enough information for each finding to be reproduced, understood, and fixed.
Report structure
Executive Summary - Security score (use ZeriFlow''s /100 as a baseline) - Number of findings by severity - Top 3 most critical issues
Findings Table
| ID | Title | Severity | Status | Owner |
|---|---|---|---|---|
| SEC-001 | Missing HSTS header | High | Open | Backend team |
| SEC-002 | Session cookie missing SameSite | Medium | Open | Auth service |
| SEC-003 | npm lodash vulnerability CVE-XXXX | High | Open | Frontend team |
Detailed Finding Format
For each finding: - Title: Short descriptive name - Severity: Critical / High / Medium / Low / Informational - Description: What the vulnerability is and why it matters - Evidence: Screenshot, curl command output, or code snippet showing the issue - Reproduction steps: Exact steps to reproduce - Remediation: Specific fix with code example if relevant - References: CVE number, OWASP page, or documentation link
Severity rating guide
Use CVSS scores or this simplified guide:
| Severity | Criteria |
|---|---|
| Critical | Direct data breach, account takeover, RCE possible |
| High | Significant business impact, exploitable with moderate effort |
| Medium | Limited impact or requires specific conditions |
| Low | Defense-in-depth improvement, unlikely to be exploited alone |
| Informational | Best practice recommendation |
Step 7: Remediation Tracking
A finding without a ticket is a finding that will not be fixed. Every finding in your report should have: - A GitHub issue, Jira ticket, or Linear ticket - Assigned owner - Target resolution date based on severity - A way to verify the fix when deployed
Suggested SLAs
| Severity | Fix timeline |
|---|---|
| Critical | 24 hours |
| High | 1 week |
| Medium | 1 sprint (2 weeks) |
| Low | Next quarter |
Verification
After deploying a fix, re-run the specific test that identified the vulnerability. For header and configuration issues, re-run the ZeriFlow scan and check that the finding is resolved. For code-level fixes, re-run Semgrep or the relevant test.
Track your overall security score over time. A quarterly ZeriFlow scan gives you a consistent, comparable number.
Recommended Tools by Phase
| Phase | Tools |
|---|---|
| Scope definition | Google Docs, Notion, Confluence |
| Automated scanning | ZeriFlow (free), OWASP ZAP, Qualys SSL Labs |
| Manual testing | Burp Suite Community, browser DevTools, curl |
| Code review | Semgrep, GitHub Advanced Security, ESLint security plugins |
| Dependency audit | npm audit, pip-audit, bundler-audit |
| Reporting | Google Docs, Notion, Jira, GitHub Issues |
| Remediation tracking | Jira, Linear, GitHub Projects |
Running Your First Audit in One Day
A focused first audit on a medium-sized application can be completed in one working day:
- Morning (2–3 hours): Define scope, run ZeriFlow scan, run SSL Labs, run OWASP ZAP
- Midday (2–3 hours): Manual authentication and authorization testing, code review for hardcoded secrets and SQL queries
- Afternoon (2–3 hours): Dependency audit, report writing, ticket creation
The output is a structured report with a prioritized fix list and a security score you can use to measure improvement.
Run a free scan at zeriflow.com/free-scan to start step 2 right now.
Summary
A website security audit follows a repeatable 7-step process: scope definition, automated scanning, manual security checks, code review, dependency audit, reporting, and remediation tracking. Start with automated tools to surface the majority of common vulnerabilities quickly, then apply manual testing to cover logic flaws and authorization bypasses that tools miss. Document findings with enough detail to be reproduced and fixed, assign owners, set SLA-based deadlines, and track your security score over time to demonstrate improvement.