Penetration Testing for Java Apps
A structured approach to testing your own application: reconnaissance, authentication and authorisation testing, injection, business logic flaws, and the tools that help.
On this page
Testing your own application the way an attacker would is the most direct way to find what static analysis and reviews miss. This is a structured walk through that, framed for testing systems you are authorised to test.
Key Takeaways
- Only test with authorisation, against staging — active testing is destructive.
- Work through a methodology; ad-hoc poking misses categories.
- Object-level authorisation is the highest-value manual test.
- Business logic flaws are invisible to scanners and often the most serious.
- Combine automated coverage with manual depth.
Reconnaissance
Map the application before attacking it. What are the endpoints, what framework, what does it expose?
# Spring Boot fingerprint — actuator, error page format, default paths
curl -s https://staging.acme.com/actuator | jq
curl -s https://staging.acme.com/error -H 'Accept: application/json'
# Enumerate declared endpoints from the OpenAPI spec
curl -s https://staging.acme.com/v3/api-docs | jq '.paths | keys'
# Look for exposed management endpoints that should be internal
for path in env beans configprops mappings heapdump threaddump; do
echo "actuator/$path: $(curl -s -o /dev/null -w '%{http_code}' https://staging.acme.com/actuator/$path)"
doneAn exposed /actuator/env or /actuator/heapdump is a finding on its own — the first leaks
configuration, the second leaks memory contents including credentials.
Record as you go, not afterwards: the exact request, the exact response, and the timestamp. A finding
nobody can reproduce on demand does not get fixed, and reconstructing "I think it was a PUT with the
identifier changed" three days later costs far more than logging it would have. Most testing proxies
keep full history — export it alongside the report so each finding ships with the request that
demonstrates it.
Authentication
# A large timing difference between valid and invalid usernames leaks which
# accounts exist. A constant-time login hashes a dummy password when the user
# is absent so both paths cost the same.
for user in valid@example.com invalid@example.com; do
curl -s -o /dev/null -w "$user: %{time_total}s\n" \
-X POST https://staging.acme.com/login \
-d "email=$user&password=wrong"
doneFor JWTs, test the classic three: change alg to none and strip the signature; try common HMAC
secrets if the token uses HS256; and attempt key confusion by signing with the server's public key as
an HMAC secret. A correctly configured resource server pins the algorithm and rejects all three — the
test proves it does.
Authorisation
This is where the highest-value findings are, and where automation is weakest:
# Obtain a resource id as user A, then request it as user B.
ALICE_TOKEN=$(login alice)
BOB_TOKEN=$(login bob)
# Alice's order id
ORDER=$(curl -s -H "Authorization: Bearer $ALICE_TOKEN" \
https://staging.acme.com/api/v1/orders | jq -r '.content[0].id')
# Bob tries to read it. A 200 is a broken-object-level-authorisation finding.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $BOB_TOKEN" \
https://staging.acme.com/api/v1/orders/$ORDERTest every operation, not just reads. A system that correctly denies reading another user's order may still allow cancelling it, updating it, or adding to it — each is a separate handler with its own authorisation check that may be missing.
Test function-level authorisation too: call admin endpoints as a regular user, and try forced browsing to paths not linked in the UI. An endpoint hidden rather than protected is not protected.
Injection
# SQL injection — a time-based probe works even with no visible output
curl -s "https://staging.acme.com/api/v1/orders?status=PLACED');SELECT+pg_sleep(5)--"
# XSS reflection
curl -s "https://staging.acme.com/search?q=<script>alert(1)</script>" | grep -o '<script>alert'
# SSRF via a URL parameter
curl -s -X POST https://staging.acme.com/api/v1/imports \
-d '{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# Path traversal in a file parameter
curl -s "https://staging.acme.com/api/v1/files?name=../../../../etc/passwd"sqlmap automates SQL injection thoroughly once you have found a candidate parameter:
sqlmap -u "https://staging.acme.com/api/v1/orders?status=PLACED" \
--headers="Authorization: Bearer $TOKEN" \
--batch --level=3 --risk=2 --technique=BTBusiness logic
Scanners cannot find these because they require understanding what the application is supposed to do:
- Order a negative quantity — does the total go negative and credit the account?
- Apply the same single-use coupon twice, in parallel, exploiting a race.
- Skip the payment step by calling the order-confirmation endpoint directly.
- Change the price in a request the server does not re-validate against the catalogue.
- Complete a workflow out of order — ship before pay, approve your own request.
- Race a balance check: withdraw the same funds from two concurrent requests.The coupon and withdrawal cases are races, and they need concurrent requests to trigger:
# Fire the same request 20 times simultaneously. If more than one succeeds
# where only one should, there is a missing lock or a check-then-act race.
seq 20 | xargs -P 20 -I{} curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
https://staging.acme.com/api/v1/coupons/SAVE50/redeemBusiness logic flaws are frequently the most serious findings, because they let an attacker abuse the application exactly as designed rather than breaking it — which means monitoring built for anomalies does not fire.
Tooling
| Tool | Purpose |
|---|---|
| OWASP ZAP | Intercepting proxy, active and passive scanning |
| Burp Suite | The professional intercepting proxy |
| sqlmap | Automated SQL injection |
| ffuf / gobuster | Endpoint and directory enumeration |
| jwt_tool | JWT attack automation |
| nuclei | Template-based vulnerability scanning |
An intercepting proxy is the foundation. Route the application's traffic through ZAP or Burp, and every request becomes something you can inspect, modify and replay — which is how most manual testing actually happens.
From findings to fixes
A penetration test that produces a PDF nobody actions is theatre. Each finding needs the same treatment as any other work: a reproduction, a severity, an owner and a fix, tracked to closure.
Turn confirmed findings into regression tests. An IDOR you found by hand becomes a @Test
asserting the 404, so the same gap cannot reopen. This is the most durable output of a test — it
converts a one-time finding into a permanent guard.
Retest after fixes. A remediation that was not verified is a finding you have chosen to believe is closed.
What to take away
Test only what you are authorised to, against staging. Work through a methodology so you cover every category rather than the ones that come to mind. Spend your manual effort on object-level authorisation and business logic, where scanners are blind. Then turn every finding into a regression test so it stays fixed.
Frequently Asked Questions
Can I run these tests against production?
Is automated scanning enough?
What is the single most valuable manual test?
Related tutorials
- Testing Spring SecurityWriting security tests that catch real gaps: @WithMockUser and @WithUserDetails, MockMvc request post-processors, testing method security, mock JWTs, and the negative tests that matter.
- DevSecOps — Securing the PipelineSecurity gates that catch real problems without blocking delivery: pre-commit secret scanning, SAST with FindSecBugs, dependency and container scanning, DAST, and tuning out the noise.
- Threat ModellingFinding design flaws before they ship: drawing data flow diagrams, applying STRIDE per element, prioritising with DREAD, and running a session that produces actionable work.
- Secrets Management & Key SecurityGetting secrets out of configuration: taking an inventory, Vault KV and dynamic database credentials, Kubernetes auth, the External Secrets Operator, and rotation that works.