On this page
The first hour on a web application is for mapping, not attacking. Everything we test later, from authentication to authorization to injection, depends on knowing which requests the app makes, which ones take input, and which ones the interface never shows.
We test only the applications, hosts and accounts named in the scope letter. Every command below assumes that letter exists.
What to check first
- Every request goes through an intercepting proxy from the first click, so the site map builds itself while we use the app normally.
- The response headers on the main page and the login page.
robots.txt,sitemap.xml,/.well-known/and the JavaScript bundles, which together usually name every API route.- The framework, server and CMS, with versions where they show.
- A test account for every role, and two accounts in the same role, so the later authorization checks have something to compare.
Security headers and cookie flags
curl -sI https://app.example.com/
curl -s -D - -o /dev/null https://app.example.com/login \
| grep -Ei 'set-cookie|content-security-policy|strict-transport|x-frame-options|server|x-powered-by'
We read the session cookie's flags, whether a Content Security Policy exists and what it allows, whether HSTS is set and for how long, and what the server says about itself.
Fix. Session cookies carry Secure, HttpOnly and SameSite=Lax or stricter. Send HSTS with a max-age of at least six months once every subdomain serves HTTPS. Write a CSP without unsafe-inline for scripts, and set frame-ancestors to stop other sites framing the app.
API routes in JavaScript bundles
base=https://app.example.com
curl -s "$base/" | grep -oE 'src="/[^"]+\.js"' | cut -d'"' -f2 > bundles.txt
while read -r js; do
curl -s "$base$js" | grep -oE "[\"'\`]/api/[A-Za-z0-9_/{}.-]+" | tr -d "\"'\`"
done < bundles.txt | sort -u > api-routes.txt
while read -r js; do
curl -s "$base$js" | tail -c 300 | grep -o 'sourceMappingURL=[^ ]*'
done < bundles.txt
Bundle names are usually hashed, so we take them from the page rather than guessing. Single page apps ship their route table to every visitor: admin routes, feature flags and internal endpoints sit in the bundle even when the interface hides them. A sourceMappingURL comment points at the source map; we fetch it and confirm it is JSON beginning with {"version":3 before calling it a finding, because the original source with comments is what it gives back.
Then we look for a published API description:
for p in openapi.json swagger.json v2/api-docs api-docs graphql; do
curl -s -o /dev/null -w "%{http_code} %{content_type} $p\n" "$base/$p"
done
A single page app answers almost any path with 200 and its own HTML shell. A JSON content type, or a body that is actually a schema, is the result; the status code alone is not.
Fix. Do not deploy source maps to production servers; upload them to your error tracker instead. Turn off GraphQL introspection in production and require authentication for API documentation. More important than either: the API must be safe when an attacker has its full description, because eventually one will.
Content discovery without soft 404s
ffuf -u "$base/FUZZ" -w wordlist.txt -ac \
-mc 200,204,301,302,401,403 -rate 20 -o ffuf.json
curl -s "$base/.git/HEAD" | grep -q '^ref: ' && echo "exposed .git"
The -ac flag calibrates against responses to random paths, so an app that returns 200 for everything does not flood the results. We keep the request rate inside the agreed limit. A 401 or 403 is a result too: it tells us a path exists and something is guarding it. A .git/HEAD that starts with ref: means the repository is exposed, and with it the source and often credentials in the history.
Fix. Deploy from build artifacts, never from a working copy, so .git, backups and editor files never reach the web root. Block dotfiles at the web server as a second layer, with an exception for /.well-known/, which standard protocols rely on.
Known vulnerabilities in the stack
Every version string we find goes on a list we check against current advisories, with the framework and its plugins first. Plugins get the same attention as the core product: several incidents in our research came in through a plugin, not the platform.
If the app runs PHP and the client gives us host access, atarus-phpcheck inventories PHP versions, server APIs and mitigating settings for the October 2026 PHP core bugs without triggering them.
Fix. Keep a dependency inventory per application, including plugins, and patch on a schedule you can state in days. Removing the X-Powered-By header reduces noise but fixes nothing.
List every input
By the end of the hour we have a working list of every parameter the app reflects, every file upload, every redirect parameter such as next= or return=, and every object ID in a URL or request body. That list feeds the authentication and session checks and the authorization tests that come next.
We run atarus-webscan alongside the manual pass for the checks that are the same on every app, so the manual time goes to the logic specific to this one.
What the defender sees
Content discovery is loud: hundreds of 404 responses from one address in a few minutes. A web application firewall may start blocking us. We agree with the client beforehand whether we test through the WAF or around it: one tests the application, the other tests the WAF.