Related articles: Also see the OWASP Top 10 vulnerabilities every QA engineer should test for, securing API endpoints as part of your application security program, and adding dynamic security scanning to your CI/CD pipeline.
Web Application Security Testing: The 10-Step Process Every QA Team Needs
In 2024, the average cost of a data breach reached $4.88 million, according to IBM's Cost of a Data Breach Report. Beyond the financial impact, security incidents erode user trust, damage brand reputation, and can lead to regulatory penalties under laws like GDPR, CCPA, and HIPAA.
Yet, despite the high stakes, many development teams treat security as an afterthought�a final checklist item before launch, if it's addressed at all. This is a dangerous mindset in a world where attackers are increasingly sophisticated, automated, and relentless.
Security testing is the practice of proactively identifying vulnerabilities in your application before attackers can exploit them. For QA engineers, developers, and founders, integrating security testing into your development lifecycle is not optional�it's essential.
In this guide, we'll cover:
- The OWASP Top 10 vulnerabilities and how to test for them
- Manual and automated security testing techniques
- Tools like OWASP ZAP, Burp Suite, Snyk, and npm audit
- How to integrate security checks into your CI/CD pipeline
- Best practices for secure development
Whether you're building a SaaS platform, an e-commerce site, or a content management system, this article will give you the knowledge and tools to protect your users and your business.
The OWASP Top 10: A Foundation for Web Security
The Open Web Application Security Project (OWASP) is a nonprofit foundation dedicated to improving software security. Their OWASP Top 10 is the most widely recognized categorization of critical web application security risks. The 2021 version (latest as of 2026) includes:
| Rank | Vulnerability | Description |
|---|---|---|
| 1 | Broken Access Control | Failures in restricting what authenticated users can do (e.g., viewing others' data). |
| 2 | Cryptographic Failures | Weak or missing encryption for sensitive data (e.g., passwords, payment info). |
| 3 | Injection | Attackers inject malicious code (SQL, NoSQL, OS commands) into inputs. |
| 4 | Insecure Design | Missing or ineffective security controls in the design phase. |
| 5 | Security Misconfiguration | Default configs, unnecessary features enabled, verbose error messages. |
| 6 | Vulnerable and Outdated Components | Using libraries/frameworks with known vulnerabilities (e.g., old npm packages). |
| 7 | Identification and Authentication Failures | Weak authentication/session management (e.g., weak passwords, session fixation). |
| 8 | Software and Data Integrity Failures | Untrusted code/data (e.g., unsecured CI/CD, insecure deserialization). |
| 9 | Security Logging and Monitoring Failures | Lack of logging, delayed detection, no alerting for suspicious activity. |
| 10 | Server-Side Request Forgery (SSRF) | Attacker tricks server into making requests to unintended locations (e.g., internal systems). |
Let's dive into the most critical vulnerabilities and how to test for them.
1. Broken Access Control
What It Is: Attackers can access resources or functions they shouldn't have permission to access.
Example: A user with userId=123 can modify their profile by sending a request to /api/users/123/profile. An attacker changes the URL to /api/users/456/profile and successfully modifies another user's data.
How to Test
Manual Test:
- Log in as a regular user.
- Note the resource IDs in URLs, cookies, or API requests.
- Try changing the IDs to access other users' data.
Automated Test with Playwright:
import { test, expect } from '@playwright/test';
test('should not allow access to other users profiles', async ({ page, request }) => {
// Login as user 123
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', 'user123@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Extract the auth token from cookies
const cookies = await page.context().cookies();
const authToken = cookies.find((c) => c.name === 'auth_token')?.value;
// Attempt to access user 456's profile with user 123's auth token
const response = await request.get('https://example.com/api/users/456/profile', {
headers: {
Cookie: `auth_token=${authToken}`,
},
});
// Should return 403 Forbidden or 404 Not Found
expect([403, 404]).toContain(response.status());
});
Prevention
- Enforce authorization checks on the server: Never trust the client.
- Use role-based access control (RBAC) or attribute-based access control (ABAC).
- Log access attempts to sensitive resources for monitoring.
2. Injection (SQL Injection, XSS)
SQL Injection
What It Is: Attackers inject malicious SQL queries into input fields, potentially reading, modifying, or deleting data.
Example:
-- Normal query
SELECT * FROM users WHERE username = 'john' AND password = 'secret';
-- Malicious input: ' OR '1'='1'; --
SELECT * FROM users WHERE username = '' OR '1'='1'; --' AND password = 'secret';
This query always returns true, bypassing authentication.
How to Test
Manual Test:
Input ' OR '1'='1'; -- into login fields, search boxes, or any user input that interacts with a database.
Automated Test with SQLMap (a penetration testing tool):
sqlmap -u "https://example.com/login" --data="username=admin&password=pass" --level=5 --risk=3
Prevention
-
Use parameterized queries/prepared statements:
// BAD: String concatenation const query = `SELECT * FROM users WHERE username = '${username}'`; // GOOD: Parameterized query const query = 'SELECT * FROM users WHERE username = ?'; const result = await db.execute(query, [username]); -
Use ORMs (e.g., Prisma, Sequelize, TypeORM) that abstract SQL and use parameterized queries by default.
Cross-Site Scripting (XSS)
What It Is: Attackers inject malicious JavaScript into web pages viewed by other users.
Example:
User submits a comment: <script>alert('XSS Attack!')</script>
If not sanitized, this script executes in every visitor's browser.
Types:
- Stored XSS: Malicious script is stored in the database (e.g., comments, posts).
- Reflected XSS: Malicious script is reflected off the server (e.g., search results).
- DOM-based XSS: Vulnerability exists in client-side JavaScript.
How to Test
Manual Test:
Input <script>alert('XSS')</script> in forms, URL parameters, and any user-generated content fields.
Automated Test with Playwright:
test('should sanitize user input to prevent XSS', async ({ page }) => {
await page.goto('https://example.com/post/create');
await page.fill('textarea[name="content"]', '<script>alert("XSS")</script>');
await page.click('button[type="submit"]');
await page.goto('https://example.com/posts');
// The script tag should be escaped and not executed
const postContent = await page.locator('.post-content').first().textContent();
expect(postContent).toContain('<script>alert("XSS")</script>'); // Should be rendered as text, not executed
});
Prevention
- Sanitize all user input on the server side before storing or displaying it.
- Use Content Security Policy (CSP) headers:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com - Escape output when rendering user content in HTML.
3. Cross-Site Request Forgery (CSRF)
What It Is: An attacker tricks a user into performing an action they didn't intend (e.g., transferring money, changing email) by exploiting their authenticated session.
Example:
A user is logged into bank.com. They visit a malicious site that contains:
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
The browser automatically includes the user's bank.com cookies, and the transfer is executed.
How to Test
Manual Test:
- Log into your application.
- Create an HTML page with a form that submits to a sensitive endpoint (e.g.,
/api/delete-account). - Open that HTML page in a browser where you're logged in.
- See if the action executes.
Prevention
-
Use CSRF tokens: Generate a unique token per session and validate it on state-changing requests.
// Server generates token const csrfToken = generateToken(); res.cookie('csrf_token', csrfToken, { httpOnly: true, sameSite: 'strict' }); // Client sends token in request header fetch('/api/delete-account', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken, }, }); -
Use SameSite cookies:
SameSite=StrictorSameSite=Laxprevents cookies from being sent in cross-origin requests.
4. Vulnerable and Outdated Components
What It Is: Using libraries, frameworks, or dependencies with known security vulnerabilities.
How to Test
npm audit (for Node.js projects):
npm audit
Output:
found 3 vulnerabilities (1 moderate, 2 high)
run `npm audit fix` to fix them, or `npm audit` for details
Snyk (comprehensive dependency scanning):
npm install -g snyk
snyk test
Snyk provides detailed reports with fix recommendations.
Prevention
- Keep dependencies up to date: Use
npm outdatedandnpm update. - Automate security checks in CI/CD:
# .github/workflows/security.yml jobs: security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: npm audit --audit-level=high - uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - Use tools like Dependabot (GitHub's automated dependency updater).
5. Security Misconfiguration
What It Is: Leaving default settings, exposing sensitive files, or providing overly detailed error messages.
Examples:
- Default admin credentials (
admin/admin) - Exposed
.envfiles or.gitdirectories - Detailed stack traces visible to users
How to Test
Manual Test:
- Try accessing
/.env,/.git,/phpinfo.php,/adminwith default credentials. - Trigger errors and see if stack traces are exposed.
Automated Test with OWASP ZAP:
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://example.com
ZAP will scan for misconfigurations, missing headers, and other issues.
Prevention
- Disable directory listings.
- Remove default accounts and enforce strong password policies.
- Use environment-specific error pages (generic messages in production, detailed logs only in development).
- Set security headers:
X-Content-Type-Options: nosniff X-Frame-Options: DENY Strict-Transport-Security: max-age=31536000; includeSubDomains
Tools for Security Testing
OWASP ZAP (Zed Attack Proxy)
Type: Free, open-source web application security scanner
Best For: Automated vulnerability scanning, penetration testing
Usage:
docker run -t owasp/zap2docker-stable zap-full-scan.py -t https://example.com -r report.html
ZAP will crawl your site and test for common vulnerabilities, producing an HTML report.
Burp Suite
Type: Commercial web vulnerability scanner (has a free Community Edition)
Best For: Manual penetration testing, intercepting and modifying HTTP requests
Features: Proxy, Scanner, Intruder (automated attacks), Repeater (manual testing)
Snyk
Type: Developer-first security platform
Best For: Dependency scanning, container scanning, IaC scanning
Integration:
# .github/workflows/security.yml
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
npm audit / yarn audit
Type: Built-in Node.js package manager tool
Best For: Quick dependency vulnerability checks
Usage:
npm audit --json > audit-report.json
Playwright for Custom Security Tests
You can use Playwright to write custom security tests for your specific application logic:
test('should prevent session fixation attack', async ({ page, context }) => {
await page.goto('https://example.com/login');
const sessionBefore = (await context.cookies()).find((c) => c.name === 'session_id')?.value;
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
const sessionAfter = (await context.cookies()).find((c) => c.name === 'session_id')?.value;
// Session ID should change after login
expect(sessionBefore).not.toEqual(sessionAfter);
});
Integrating Security Testing into CI/CD
Security testing should be automated and continuous, not a one-time audit.
Example GitHub Actions Workflow:
name: Security Checks
on:
pull_request:
push:
branches:
- main
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '22'
- run: npm ci
- run: npm audit --audit-level=high
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
zap-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run build
- run: npm run start &
- run: npx wait-on http://localhost:3000
- run: docker run -t owasp/zap2docker-stable zap-baseline.py -t http://host.docker.internal:3000
This workflow runs on every pull request, catching vulnerabilities early.
The Shift-Left Security Mindset
Security should not be the responsibility of a single team or a final gate before deployment. It should be integrated throughout the development lifecycle:
- Design Phase: Threat modeling, security requirements
- Development: Secure coding practices, code reviews
- Testing: Automated security scans, manual penetration testing
- Deployment: Security headers, least-privilege access
- Production: Monitoring, logging, incident response
This is known as DevSecOps�embedding security into every stage of DevOps.
Conclusion
Security testing is not a one-time checklist�it's an ongoing practice. By understanding the OWASP Top 10, using automated tools like ZAP and Snyk, writing custom security tests with Playwright, and integrating security checks into your CI/CD pipeline, you can dramatically reduce your attack surface and protect your users.
Remember: every line of code is a potential vulnerability. The question is not whether you'll be targeted�it's whether you'll be ready.
Start securing your application today. Sign up for ScanlyApp and integrate comprehensive security testing into your QA workflow.
