Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Advanced6 min readUpdated
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?

terminal
# 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)"
done

An 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

Each authentication component is a separate line of attack. JWT handling is where the most damaging flaws tend to be.
user enumeration by timing
# 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"
done

For 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:

IDOR / BOLA testing
# 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/$ORDER

Test 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

injection probes
# 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:

terminal
sqlmap -u "https://staging.acme.com/api/v1/orders?status=PLACED" \
  --headers="Authorization: Bearer $TOKEN" \
  --batch --level=3 --risk=2 --technique=BT

Business logic

Scanners cannot find these because they require understanding what the application is supposed to do:

business logic tests
- 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:

race condition probe
# 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/redeem

Business 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

ToolPurpose
OWASP ZAPIntercepting proxy, active and passive scanning
Burp SuiteThe professional intercepting proxy
sqlmapAutomated SQL injection
ffuf / gobusterEndpoint and directory enumeration
jwt_toolJWT attack automation
nucleiTemplate-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?
Only with explicit written authorisation and against a staging environment that mirrors it. Active testing sends malformed and malicious requests that can corrupt data, trigger alerts and cause outages. Test staging, and treat any production testing as a formal engagement with a defined scope.
Is automated scanning enough?
No. Scanners find known patterns — injection, missing headers, outdated libraries — reliably and cheaply. They do not find business logic flaws, broken object-level authorisation with non-obvious ownership, or multi-step abuse, which are where the serious vulnerabilities usually are. Automation plus manual testing, not one or the other.
What is the single most valuable manual test?
Object-level authorisation. Take a resource id that belongs to one user, authenticate as another, and try to read or modify it. It is the most common serious API vulnerability and the one scanners miss because they do not know your ownership model.

Related tutorials