Can Autonomous Security Testing Break Your Application? Safety Rails, State Management, and Rate Limits
See how Ostorlab Agentic Deep Scan pairs multi-step vulnerability investigation with hard scope, network, approval, and rate controls, walked through step by step on an isolated vulnbank.org benchmark.
Before running an autonomous security agent against staging or production, teams ask the same practical questions: will it follow a link out of scope, dump customer rows from a database, lock out real accounts, or leave broken state behind?
Operational risk is not unique to AI agents. Traditional signature-based scanners send fixed payloads and predefined workflows that can still mutate data, trigger outbound emails or webhooks, or overload fragile endpoints. What changes with autonomous testing is dynamic action selection: an agent reads HTTP responses, discovers new parameters, and chooses follow-up requests across long sessions. That autonomy makes it much better at uncovering multi-step bugs, and it also means guardrails cannot live solely inside an English prompt.
Short answer
Agentic Deep Scan separates safety into three layers: system-enforced limits outside the model (IP blocklists, URL scope regexes, pre-dispatch scheme/IP/metadata checks in HTTP tools, queries-per-second caps, and request budgets), configurable human approvals (Ask me, block if no answer), and agent verification rules observed in our isolatedvulnbank.orgbenchmark (owner-scoped SQL checks, read-only debugger inspection, throwaway accounts, and 20-request rate sampling).
Why Prompts Break Down and How Enforcement Sits Outside the Model
Prompt instructions like "stay in scope" or "avoid destructive actions" drift over long sessions. We learned this firsthand in an earlier scope-drift post-mortem: when an autonomous agent hit HTTP 403 errors at a client's API gateway, it spotted a vendor clue in the response, looked up related domains, convinced itself the vendor hosted the backend, and wandered outside the intended perimeter.
A reasoning model cannot be its own sandbox. In subsequent platform hardening, we also caught and fixed a profile-configuration bug where an on-premises web scan template omitted the crawler's scope regex parameter (scope_urls_regex), and a permissive empty-regex check let discovered internal and external links pass through crawling. We aligned on-premises and cloud template parameters, tightened empty-scope handling so missing scope regexes fail closed, and added profile-gating regression tests.
Today, every candidate action passes through three distinct layers before a probe reaches the target, followed by a separate validation pass before any finding enters a report:
At the first layer, system and tool code outside the model drops bad requests before dispatch. HTTP and network tools restrict URL schemes to http and https (blocking file://, ftp://, and gopher://), block cloud metadata hosts (169.254.169.254 and metadata.google.internal), and reject loopback, link-local, unspecified, and reserved IPs. Alongside those tool checks, scans enforce scan-creator IP blocklists, crawler and discovery URL scope regexes, adaptive QPS limits, and per-agent request budgets.
At the second layer, configurable Guardrails approvals let operators choose Ask me, block if no answer during scan setup (covered in Your AI Pentester Now Asks Before It Takes a Risky Action). Whenever the agent needs to reach real people, alter data destructively, or resolve a scope question, it pauses for human confirmation, and unanswered scope questions fail closed.
At the third layer, planner and executor verification rules shape how the agent proves bugs once a target is in scope. Unlike IP blocklists or scheme checks, these rules are not per-query firewall filters; they guide how the agent designs minimal proofs.
Walking Through the Safeguards on vulnbank.org
To see how those verification policies behave in practice, consider an authorized benchmark scan of Agentic Deep Scan against vulnbank.org, an isolated sandbox banking application seeded with vulnerabilities. Running in an isolated sandbox matters: the walkthrough below shows how the agent bounded its probes during that scan, while system-enforced controls and operator prerequisites govern live targets.
Proving Blind SQL Injection Without Dumping Customer Rows
Early in discovery, a single-quote probe against POST /api/v3/forgot-password triggered an HTTP 500 error exposing the server-side query shape:
SELECT id FROM users WHERE username='<userinput>'
Matching usernames returned HTTP 200 (Reset PIN has been sent...) while non-matching values returned HTTP 404 (User not found), giving the agent a clean boolean oracle.
With a blind boolean oracle on a users table, an unconstrained script often walks row by row dumping emails, password hashes, or reset tokens across real customers. Instead, the agent kept its proof strictly scoped to metadata and a single test row it controlled (baseline_probe_user_123). First it queried information_schema.columns to confirm the password column existed on users without reading any rows. Then it verified only the first two password characters ('B' and 'a') strictly where username='baseline_probe_user_123', and stopped:
# 1. Confirm column existence via information_schema count (zero row data read)
curl -s -X POST https://vulnbank.org/api/v3/forgot-password \
-H "Content-Type: application/json" \
-d '{"username":"'\'' OR (SELECT count(*) FROM information_schema.columns WHERE table_name='\''users'\'' AND column_name='\''password'\'')=1-- -"}'
# -> HTTP 200 TRUE
# 2. Verify character 1 on the agent's own test row
curl -s -X POST https://vulnbank.org/api/v3/forgot-password \
-H "Content-Type: application/json" \
-d '{"username":"baseline_probe_user_123'\'' AND (SELECT substring(password from 1 for 1) FROM users WHERE username='\''baseline_probe_user_123'\'')='\''B'\''-- -"}'
# -> HTTP 200 TRUE (first char 'B' on baseline_probe_user_123)
# 3. Verify character 2 on the agent's own test row, then stop
curl -s -X POST https://vulnbank.org/api/v3/forgot-password \
-H "Content-Type: application/json" \
-d '{"username":"baseline_probe_user_123'\'' AND (SELECT substring(password from 2 for 1) FROM users WHERE username='\''baseline_probe_user_123'\'')='\''a'\''-- -"}'
# -> HTTP 200 TRUE (second char 'a' on baseline_probe_user_123)
Paired with the boolean oracle and database identity (current_user=postgres, current_database()=vulnerable_bank), this owner-scoped check proved High severity without touching customer data. On live targets where teams want explicit sign-off before even bounded extraction runs, enabling Ask me, block if no answer pauses those steps for approval.
Stopping at the Werkzeug Debugger PIN Gate
Elsewhere on vulnbank.org, GET /console served an unauthenticated Werkzeug debug console (Flask==2.0.1, Werkzeug==2.0.1, debug=True):
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<title>Console // Werkzeug Debugger</title>
<script type="text/javascript">
var SECRET = "h01dGBYJuNCEMvNGSaWE";
var EVALEX = true;
var EVALEX_TRUSTED = false;
</script>
That read-only HTML response established two facts and one hard boundary. First, /console was reachable without authentication (HTTP 200) with interactive code evaluation enabled (EVALEX = true) and a per-page anti-CSRF SECRET present. Second, EVALEX_TRUSTED = false meant Werkzeug's PIN gate was still active: executing Python on the host required obtaining, deriving, or brute-forcing the debugger PIN.
Rather than hammering the debugger with PIN guesses or attempting host execution, the agent documented the configuration and stopped. Being precise about that boundary matters: the benchmark scan scored the public RCE surface Critical because of the blast radius if paired with a leaked or cracked PIN, whereas strictly standalone verified evidence without a PIN bypass establishes High-severity RCE exposure with the PIN prerequisite unresolved.
Using a Throwaway Account, and Knowing What State Still Persists
When testing signup, login, and session handling, the agent avoided real user credentials and registered a throwaway account (testuser_session_001):
# Step 1: Self-register a test identity
curl -s -X POST https://vulnbank.org/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser_session_001","password":"SecurePass123!","email":"agent_test@sandbox.local"}'
# Step 2: Obtain a JWT and verify authenticated access
curl -s -X POST https://vulnbank.org/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser_session_001","password":"SecurePass123!"}'
curl -s -H "Authorization: Bearer <token>" https://vulnbank.org/dashboard
# -> HTTP/2 200 OK
# Step 3: Probe candidate logout endpoints and inspect decoded JWT claims
curl -v -X POST https://vulnbank.org/logout
# -> HTTP/2 404 Not Found (same 404 across GET /logout, POST /api/logout, POST /session/invalidate, POST /auth/logout)
# Decoded payload: {"user_id":5816,"username":"testuser_session_001","is_admin":false,"iat":1774532021} (no exp claim)
Reading those outputs carefully prevents overclaiming what the test proved. Because POST /logout and all four alternative paths (GET /logout, POST /api/logout, POST /session/invalidate, and POST /auth/logout) returned HTTP 404, no logout ever ran on the server. Getting HTTP 200 on /dashboard afterward only confirms that none of those five guessed routes terminated the session, not that tokens survive a real logout. What the response did prove directly came from decoding the HS256 JWT: the payload carried iat with no exp claim, while server-side token TTL or revocation remained untested.
Equally important is separating identity separation from application-state isolation. Registering testuser_session_001 kept the agent off genuine users' accounts, yet it did not eliminate state mutations. On a customer target, self-registration still writes database rows, generates audit logs, and can fire welcome emails or webhooks unless integrations are turned off. An isolated sandbox resets cleanly after a benchmark; live applications do not, so staging isolation or email and webhook suppression remain operator responsibilities.
Sampling Password-Reset Rate Limits Instead of Flooding the Endpoint
Finally, static context on vulnbank.org showed that password resets generated a 3-digit PIN (reset_pin = random.randint(0, 999) at app.py:1164, covering 000 to 999). Trying all 1,000 values against a live endpoint risks locking out target accounts, generating password-reset email spam, lighting up security logs with noisy alerts, or tripping WAF IP bans.
Instead of firing 1,000 guesses, the agent ran a bounded 20-attempt sample against POST /verify_reset_pin (illustrated below with the sandbox email, zero-padded 3-digit PINs, and response-header inspection):
# Bounded 20-request probe against POST /verify_reset_pin
for i in {1..20}; do
pin=$(printf "%03d" "$i")
curl -s -D - -o /dev/null -X POST "https://vulnbank.org/verify_reset_pin" \
-H "Content-Type: application/json" \
-d "{\"email\":\"agent_test@sandbox.local\",\"pin\":\"$pin\"}"
done
# -> HTTP/1.1 200 OK (no Retry-After or X-RateLimit-* throttling headers observed across 20 attempts in 1.74 s)
All 20 requests finished in 1.74 seconds (~11.5 requests per second) with no throttling observed within the 20-attempt sample. Extrapolating that pace to ~87 seconds for 1,000 combinations is strictly a conditional linear projection: it assumes steady throughput, no higher-threshold lockout or per-IP limit kicking in after attempt 20, no progressive cooldown delays, and a PIN lifetime longer than the run. In the benchmark scan, combining static awareness of the 000 to 999 PIN range with 20 unthrottled requests supported a High severity rating, whereas runtime evidence alone proves only what happened across those 20 probes. On live targets, keeping rate probes small lowers lockout and alert risk, though it cannot guarantee zero WAF or monitoring triggers under arbitrary target rules.
What Operators Should Still Configure Before Live Scans
Autonomous agents work best when infrastructure controls, operator settings, and agent verification rules reinforce one another rather than substituting for one another.
Before running against staging or production, teams should separate what the platform enforces from what target owners configure:
- Enforced outside the model: HTTP/network scheme allowlisting (
http/https), private/loopback/reserved/metadata IP rejection, scan-creator IP blocklists, fail-closedscope_urls_regexfiltering, adaptive QPS throttling, and per-agent request caps. - Configured by the operator: explicit target allowlists and excluded environments,
Ask me, block if no answerapproval gates for destructive steps or real-user messaging, and staging isolation or suppressed webhooks/emails when self-registration or write tests are enabled. - Verified by evidence boundaries: bounded SQL checks, read-only debug-surface inspection, separate validation re-tests, and explicit distinctions between confirmed runtime observations and conditional extrapolations or unbypassed gates.
Explore how these controls fit together in Ostorlab Agentic Deep Scan for Web Applications & APIs and Mobile Applications.
FAQ
Can an autonomous security testing agent disrupt a live application?
Both signature-based scanners and autonomous agents can cause side effects if pointed at live targets without operational limits. Agentic Deep Scan reduces risk through system-enforced controls outside the model (IP blocklists, URL scope regexes, SSRF and cloud-metadata checks, QPS caps, and request budgets), optional human approvals (Ask me, block if no answer), and bounded verification rules, paired with staging targets or suppressed webhooks for state-changing tests.
How does Ostorlab enforce scope boundaries outside the LLM?
Crawling and discovery configurations filter URLs against scan-profile scope regexes (scope_urls_regex) that fail closed when missing, scans enforce system-level IP blocklists and adaptive QPS limits, HTTP tools allow only http/https schemes and block loopback, link-local, reserved, and cloud metadata IPs before dispatch, and Guardrails approvals treat unanswered scope questions as out of scope.
Why verify blind SQL injection on a self-owned account instead of dumping user rows?
Dumping customer rows turns testing into exfiltration. Confirming users.password via information_schema.columns and verifying two characters strictly on the agent's own baseline_probe_user_123 row substantiated High impact without touching other users' data, and operators can require human approval before exploitation steps run.
Why did the Werkzeug debugger check stop at /console instead of executing commands?
Read-only inspection confirmed GET /console returned HTTP 200 with EVALEX = true, the per-page SECRET, and EVALEX_TRUSTED = false (showing the PIN gate is active). The benchmark scan rated the exposed RCE surface Critical, while keeping verified surface exposure clearly separated from an unverified PIN leak or bypass.
What did the 20-request rate-limit probe actually establish?
All 20 zero-padded attempts against POST /verify_reset_pin completed in 1.74 s (~11.5 req/s) with no throttling observed within the 20-attempt sample, limiting lockout and reset-spam risk. Assuming steady throughput, no higher-threshold lockout past attempt 20, and sufficient PIN validity, a conditional linear projection estimates ~87 seconds for 1,000 guesses.
Does using a throwaway account eliminate all state side effects?
No. Registering testuser_session_001 avoided real user credentials while checking five candidate /logout routes (all 404, meaning no logout ran) and inspecting a JWT missing an exp claim. Self-registration still inserts rows and audit logs on the target app, so live environments still require staging isolation or webhook and email suppression.
References
- Ostorlab: Post-Mortem: Why Autonomous AI Agents Escape Scope and How to Contain Them
- Ostorlab: Your AI Pentester Now Asks Before It Takes a Risky Action
- Ostorlab: Does AI Security Testing Hallucinate? LLM Guesses vs. Proof-of-Exploit
- Ostorlab: AI Can Run the Attack. Can You Trust the Result?
- OWASP API Security Top 10: API4:2023 Unrestricted Resource Consumption
- CWE-307: Improper Restriction of Excessive Authentication Attempts
- CWE-613: Insufficient Session Expiration