Most developers think of penetration testing as something a separate security team does. Someone in a hoodie runs Kali Linux, sends you a PDF full of findings, and you spend the next sprint fixing things you could have caught months ago. I’ve been on both sides of that handoff, and the truth is: the earlier you test, the cheaper the fix.
You don’t need to become a security researcher. But if you can think like an attacker for 30 minutes during development, you’ll catch the bugs that matter — auth bypasses, injection flaws, exposed internal endpoints — before they make it to production.
This is Part 12 of our Cloud Security Engineering series. We’re going hands-on with penetration testing from a developer’s perspective.
Why Developers Should Pen Test
There’s a persistent myth that pen testing requires deep expertise and specialized certifications. While professional pen testers absolutely bring depth, the most common vulnerabilities in web applications are straightforward:
- Broken authentication — endpoints that don’t check tokens, or check them incorrectly
- Injection — SQL, NoSQL, command injection through unsanitized inputs
- IDOR — accessing another user’s data by changing an ID in the URL
- SSRF — making the server fetch internal resources through user-controlled URLs
- XSS — injecting scripts through input fields that render in other users’ browsers
These aren’t exotic zero-days. They’re logic bugs that developers introduce and developers can find. The advantage you have over an external pen tester is context — you know the codebase, the data model, the auth flow. You know which endpoints were rushed, which ones skip validation, which ones were “temporary.”
The Pen Testing Methodology
Professional penetration testing follows a structured methodology. Even as a developer doing lightweight security testing, following this structure keeps you focused and thorough.
The five phases are:
- Scope — Define what you’re testing. For developers, this is usually your own service, a specific set of endpoints, or a feature branch before merge.
- Reconnaissance — Gather information about the target. What endpoints exist? What technologies are in use? What does the API surface look like?
- Scanning — Use automated tools to find known vulnerabilities, open ports, misconfigurations.
- Exploitation — Attempt to actually exploit what you found. Can you bypass auth? Can you inject SQL? Can you access another user’s data?
- Reporting — Document findings with severity, reproduction steps, and remediation guidance.
The key difference from hacking: everything is authorized, scoped, and documented. You’re testing your own applications in environments you control.
Reconnaissance Phase
Recon is about building a map of your attack surface. For your own application, start with what an outsider would see.
Network Reconnaissance with nmap
nmap is the standard tool for network discovery and port scanning. Use it against your staging or development environment (never production without explicit authorization):
# Basic service scan — what ports are open and what's running
nmap -sV -p 1-10000 staging.yourapp.com
# Output:
# PORT STATE SERVICE VERSION
# 22/tcp open ssh OpenSSH 8.9
# 80/tcp open http nginx 1.24.0
# 443/tcp open ssl/http nginx 1.24.0
# 5432/tcp open postgresql PostgreSQL 15.2
# 6379/tcp open redis Redis 7.0.11That scan just told you the staging database and Redis are exposed to the network. That’s a finding before you’ve even looked at the application code. In production, ports 5432 and 6379 should never be reachable from outside the VPC.
# OS detection and script scanning for more detail
nmap -A -T4 staging.yourapp.com
# Check for specific vulnerabilities with NSE scripts
nmap --script=http-sql-injection -p 443 staging.yourapp.comAPI Surface Discovery
Before scanning, enumerate your API surface. If you have an OpenAPI spec, great. If not, check your routes:
# If you use Express/Fastify, dump routes
curl -s https://staging.yourapp.com/api/docs | jq '.paths | keys[]'
# Or check your framework's route listing
python manage.py show_urls # Django
rails routes # RailsDocument every endpoint, its HTTP method, required authentication, and expected input. This becomes your testing checklist.
Common Attack Vectors for Developers
These are the categories that show up in most web application pen tests. As a developer, you should test for all of them.
1. Authentication Bypass
The most impactful bugs. Test whether endpoints actually enforce auth:
# Test 1: Access protected endpoint without a token
curl -s -o /dev/null -w "%{http_code}" \
https://staging.yourapp.com/api/admin/users
# Expected: 401 or 403
# Bad: 200 (endpoint doesn't check auth)
# Test 2: Access with an expired token
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer eyJhbGciOi...expired..." \
https://staging.yourapp.com/api/admin/users
# Expected: 401
# Bad: 200 (token expiry not validated)
# Test 3: Access with a valid user token on an admin endpoint
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $REGULAR_USER_TOKEN" \
https://staging.yourapp.com/api/admin/users
# Expected: 403
# Bad: 200 (no role check — just checks "is logged in")2. Insecure Direct Object References (IDOR)
IDOR happens when you can access another user’s resources by changing an ID:
# You're user 42. Try accessing user 43's data.
curl -H "Authorization: Bearer $USER_42_TOKEN" \
https://staging.yourapp.com/api/users/43/profile
# Expected: 403
# Bad: 200 with user 43's data
# Try sequential IDs to see if any leak
for id in $(seq 1 100); do
status=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $USER_42_TOKEN" \
"https://staging.yourapp.com/api/orders/$id")
[ "$status" = "200" ] && echo "IDOR: order $id accessible"
done3. SQL Injection
Even with ORMs, injection can happen through raw queries, search functions, or sort parameters:
# Classic SQL injection test
curl "https://staging.yourapp.com/api/search?q=test'%20OR%201=1--"
# Time-based blind injection
curl "https://staging.yourapp.com/api/search?q=test'%20AND%20SLEEP(5)--"
# If the response takes 5 seconds, the parameter is injectable4. Server-Side Request Forgery (SSRF)
If your app fetches URLs provided by users (webhooks, image URLs, import features), test for SSRF:
# Test if the server will fetch internal metadata
curl -X POST https://staging.yourapp.com/api/webhooks \
-H "Content-Type: application/json" \
-d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# Bad: returns AWS credentials from the metadata serviceTools of the Trade
OWASP ZAP — Automated Scanning
ZAP is the best free, open-source web application scanner. It catches low-hanging fruit automatically.
# Run ZAP in headless mode against your staging environment
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://staging.yourapp.com \
-r /tmp/zap-report.html
# Run an API scan if you have an OpenAPI spec
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
-t https://staging.yourapp.com/api/openapi.json \
-f openapi \
-r /tmp/zap-api-report.html
# Run a full active scan (takes longer, more thorough)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
-t https://staging.yourapp.com \
-r /tmp/zap-full-report.htmlZAP’s baseline scan is fast enough to run in CI. The API scan is particularly useful if you maintain an OpenAPI spec — it tests every documented endpoint automatically.
Burp Suite — Manual Testing
Burp Suite (Community Edition is free) is the industry standard for interactive testing. It works as a proxy between your browser and the target:
- Proxy — Intercept and modify requests in real-time. Change IDs, remove auth headers, modify payloads.
- Repeater — Replay modified requests quickly. Essential for testing auth bypass and injection.
- Intruder — Automate parameter fuzzing. Feed it a list of injection payloads and watch for anomalies.
- Scanner (Pro only) — Automated vulnerability scanning similar to ZAP.
The workflow: browse your app normally through Burp’s proxy, then go to the Site Map and systematically test each endpoint in Repeater with modified inputs.
nmap — Network Layer
We covered nmap above. The key use cases for developers:
- Verify that only expected ports are open in staging/production
- Check TLS configuration with
nmap --script ssl-enum-ciphers - Detect unnecessary services that increase attack surface
Writing Security Test Cases
The biggest win: turn your pen test findings into automated tests that run in CI. Here’s how to write security test cases that catch regressions.
Python (pytest)
import pytest
import requests
BASE_URL = "https://staging.yourapp.com"
class TestAuthSecurity:
"""Security tests for authentication and authorization."""
def test_admin_endpoint_requires_auth(self):
"""Admin endpoints must reject unauthenticated requests."""
response = requests.get(f"{BASE_URL}/api/admin/users")
assert response.status_code in (401, 403), \
f"Admin endpoint accessible without auth: {response.status_code}"
def test_admin_endpoint_rejects_regular_user(self):
"""Admin endpoints must reject non-admin users."""
headers = {"Authorization": f"Bearer {get_regular_user_token()}"}
response = requests.get(
f"{BASE_URL}/api/admin/users", headers=headers
)
assert response.status_code == 403, \
f"Regular user can access admin endpoint: {response.status_code}"
def test_idor_user_profile(self):
"""Users must not access other users' profiles."""
headers = {"Authorization": f"Bearer {get_user_token(user_id=42)}"}
response = requests.get(
f"{BASE_URL}/api/users/43/profile", headers=headers
)
assert response.status_code == 403, \
f"IDOR vulnerability: user 42 can access user 43's profile"
def test_sql_injection_search(self):
"""Search endpoint must not be vulnerable to SQL injection."""
headers = {"Authorization": f"Bearer {get_regular_user_token()}"}
payloads = [
"' OR 1=1--",
"'; DROP TABLE users;--",
"' UNION SELECT * FROM users--",
]
for payload in payloads:
response = requests.get(
f"{BASE_URL}/api/search",
params={"q": payload},
headers=headers,
)
# Should return 400 (bad input) or 200 with no data leak
assert response.status_code != 500, \
f"SQL injection caused server error with payload: {payload}"
if response.status_code == 200:
data = response.json()
assert len(data.get("results", [])) == 0, \
f"SQL injection returned data with payload: {payload}"JavaScript (Jest)
const axios = require('axios');
const BASE_URL = 'https://staging.yourapp.com';
describe('Security: Authentication & Authorization', () => {
test('admin endpoint rejects unauthenticated requests', async () => {
try {
await axios.get(`${BASE_URL}/api/admin/users`);
fail('Admin endpoint should not return 200 without auth');
} catch (error) {
expect([401, 403]).toContain(error.response.status);
}
});
test('users cannot access other users data (IDOR)', async () => {
const token = await getTokenForUser(42);
try {
await axios.get(`${BASE_URL}/api/users/43/profile`, {
headers: { Authorization: `Bearer ${token}` },
});
fail('IDOR: user 42 should not access user 43 profile');
} catch (error) {
expect(error.response.status).toBe(403);
}
});
test('SSRF: webhook URL cannot target internal services', async () => {
const token = await getAdminToken();
const internalUrls = [
'http://169.254.169.254/latest/meta-data/',
'http://localhost:6379/',
'http://10.0.0.1:5432/',
];
for (const url of internalUrls) {
try {
const resp = await axios.post(
`${BASE_URL}/api/webhooks`,
{ url },
{ headers: { Authorization: `Bearer ${token}` } }
);
expect(resp.status).not.toBe(200);
} catch (error) {
expect([400, 422]).toContain(error.response.status);
}
}
});
});These tests are simple, readable, and catch the most common security regressions. Add them to your CI pipeline and run them against staging after every deploy.
Authorized Testing Guidelines
This is critical: penetration testing without authorization is illegal in most jurisdictions. Always follow these rules:
- Written authorization — Get explicit written permission before testing. Even for your own company’s systems, have it documented.
- Scope boundaries — Define exactly what you’re testing. Don’t scan systems outside your scope.
- Environment — Test in staging or dedicated security testing environments. Never run active scans against production unless explicitly authorized.
- Third-party services — If your app integrates with external APIs (Stripe, AWS, etc.), don’t pen test their infrastructure. Test your integration logic only.
- Data handling — If you discover sensitive data during testing, report it through proper channels. Don’t copy, store, or share it.
- Bug bounty programs — If you find vulnerabilities in other services, check if they have a bug bounty program and follow their responsible disclosure policy.
For your own development workflow, create a dedicated testing environment that mirrors production. Run automated ZAP scans in CI, manual Burp Suite sessions during sprint reviews, and security test suites on every deploy.
Key Takeaways
Start with automated scanning. Run OWASP ZAP’s baseline scan in your CI pipeline. It catches XSS, missing security headers, and common misconfigurations with zero effort after setup.
Test auth on every endpoint. The curl-based tests above take minutes to write and catch the most impactful bugs. Every new endpoint should have a “what happens without auth?” test.
Think in attack vectors, not features. When you build a user profile page, don’t just test that user 42 can see their data. Test that user 42 cannot see user 43’s data. Test what happens with no token. Test what happens with an admin token.
Automate your findings. Every bug you find manually should become an automated test case. This prevents regressions and builds institutional security knowledge.
Stay authorized. Test your own systems, in your own environments, with documented permission. The goal is to make your software more secure, not to break things you shouldn’t be touching.
Penetration testing isn’t a dark art. It’s methodical, structured, and increasingly a core developer skill. Start with the tools and techniques in this post, and you’ll catch the majority of common web application vulnerabilities before they reach production.











