While/Until Loop Implementation
Epic: #3
Phase: 2 - Advanced Loops (v0.2)
Priority: High
Estimated Effort: 3-4 days
Summary
Implement while and until loops that continue execution based on dynamic conditions evaluated at runtime, enabling adaptive attack patterns that respond to target behavior.
Motivation
Fixed-iteration loops (loop.iterations) are limited for scenarios where:
- You don't know how many iterations are needed upfront
- Loop continuation depends on response data
- Attack should adapt to target state changes
- Testing requires exhaustive exploration until a condition is met
Current limitation: Cannot model "keep trying until balance == 0" or "drain all pages of results".
Real-World Scenarios
- Data Exfiltration: Paginate through all records until no more pages
- Account Draining: Keep withdrawing funds until balance reaches zero
- Rate Limit Discovery: Send requests until rate limited
- Privilege Escalation: Try elevation until admin access granted
- Resource Exhaustion: Create items until quota reached
Proposed Syntax
While Loop (Continue While True)
states:
drain_account:
description: "Withdraw funds until account is empty"
request: |
POST /api/withdraw
Authorization: Bearer {{ token }}
{"amount": 100}
extract:
balance:
type: jpath
pattern: "$.account.remaining_balance"
success:
type: jpath
pattern: "$.success"
while:
condition: "{{ balance > 0 and success }}"
max_iterations: 1000 # Safety limit
timeout_seconds: 60 # Time limit
check_interval_ms: 0 # Immediate (default)
next:
- on_loop_complete:
goto: verify_account_drained
- on_timeout:
goto: drain_incomplete
- on_max_iterations:
goto: safety_limit_reached
Until Loop (Continue Until True)
states:
find_admin_user:
description: "Enumerate users until admin found"
request: |
GET /api/users/{{ loop.counter }}
Authorization: Bearer {{ token }}
extract:
role:
type: jpath
pattern: "$.user.role"
exists:
type: jpath
pattern: "$.exists"
until:
condition: "{{ role == 'admin' or not exists }}"
max_iterations: 10000
timeout_seconds: 300
next:
- when:
- condition: "{{ role == 'admin' }}"
goto: admin_found
- otherwise:
goto: no_admin_found
While vs Until
| Feature |
While |
Until |
| Continues when |
Condition is TRUE |
Condition is FALSE |
| Stops when |
Condition becomes FALSE |
Condition becomes TRUE |
| Use case |
"Keep doing while X is true" |
"Keep doing until X is true" |
| Equivalent |
while condition |
while not condition |
Example Comparison
# While loop - continue while has_more is true
while:
condition: "{{ has_more }}"
# Until loop - continue until has_more is false (equivalent)
until:
condition: "{{ not has_more }}"
Loop Configuration Parameters
Required Parameters
condition (string)
Jinja2 expression evaluated as boolean.
while:
condition: "{{ balance > 0 }}"
Supported expressions:
- Variable comparisons:
{{ x > 10 }}, {{ role == 'admin' }}
- Boolean logic:
{{ a and b }}, {{ x or y }}, {{ not z }}
- Membership:
{{ 'admin' in roles }}
- Function calls:
{{ balance | int > 0 }}
- Complex:
{{ (balance > 0) and (attempts < 10) }}
Optional Parameters
max_iterations (integer)
Maximum number of iterations before forced exit (safety limit).
while:
condition: "{{ true }}" # Infinite without limit
max_iterations: 1000
Default: 1000
Range: 1 to 100,000
timeout_seconds (integer)
Maximum execution time for entire loop.
while:
condition: "{{ has_more }}"
timeout_seconds: 300 # 5 minutes max
Default: 300 (5 minutes)
Range: 1 to 3600
check_interval_ms (integer)
Delay between condition checks (useful for polling).
while:
condition: "{{ job_status != 'completed' }}"
check_interval_ms: 1000 # Check every second
Default: 0 (no delay)
Range: 0 to 60000
stop_on_error (boolean)
Stop loop if request fails.
while:
condition: "{{ balance > 0 }}"
stop_on_error: true # Stop on first HTTP error
Default: false
min_iterations (integer)
Minimum iterations before condition is checked.
while:
condition: "{{ response_time < 100 }}"
min_iterations: 10 # Warm-up phase
Default: 1
Implementation Details
Condition Evaluation
from jinja2 import Environment, Template
class WhileLoopExecutor:
def __init__(self, template_engine):
self.template_engine = template_engine
def evaluate_condition(self, condition_expr, context):
"""Evaluate Jinja2 condition expression as boolean."""
try:
# Render the condition expression
template = self.template_engine.from_string("{{ " + condition_expr + " }}")
result = template.render(context)
# Convert to boolean
return self.to_boolean(result)
except Exception as e:
raise ConditionEvaluationError(
f"Failed to evaluate condition: {condition_expr}",
original_error=e
)
def to_boolean(self, value):
"""Convert various types to boolean."""
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes')
if isinstance(value, (int, float)):
return value != 0
return bool(value)
Loop Execution
import time
from datetime import datetime, timedelta
class WhileLoopExecutor:
def execute_while_loop(self, state_config, context):
"""Execute while loop with all safety checks."""
loop_config = state_config['while']
# Extract configuration
condition_expr = loop_config['condition']
max_iterations = loop_config.get('max_iterations', 1000)
timeout_seconds = loop_config.get('timeout_seconds', 300)
check_interval = loop_config.get('check_interval_ms', 0) / 1000
stop_on_error = loop_config.get('stop_on_error', False)
min_iterations = loop_config.get('min_iterations', 1)
# Initialize tracking
iteration = 0
start_time = datetime.now()
timeout_time = start_time + timedelta(seconds=timeout_seconds)
results = []
# Loop execution
while True:
iteration += 1
# Check max iterations
if iteration > max_iterations:
return self.complete_loop(
results,
exit_reason='max_iterations',
context=context
)
# Check timeout
if datetime.now() > timeout_time:
return self.complete_loop(
results,
exit_reason='timeout',
context=context
)
# Update loop context
context['loop'] = {
'index': iteration - 1,
'counter': iteration,
'iteration': iteration,
'first': iteration == 1,
}
# Execute request
try:
response = self.execute_request(state_config, context)
results.append(response)
# Update context with extracted values
extracted = self.extract_values(state_config, response, context)
context.update(extracted)
except Exception as e:
if stop_on_error:
raise
results.append(LoopIterationError(iteration=iteration, error=e))
# Check condition (after min_iterations)
if iteration >= min_iterations:
condition_met = self.evaluate_condition(condition_expr, context)
# While loop: stop when condition is FALSE
if not condition_met:
return self.complete_loop(
results,
exit_reason='condition_false',
context=context
)
# Inter-iteration delay
if check_interval > 0:
time.sleep(check_interval)
def execute_until_loop(self, state_config, context):
"""Execute until loop (inverse of while)."""
# Convert until to while by inverting condition
until_config = state_config['until']
# Create equivalent while config
while_config = {
**until_config,
'condition': f"not ({until_config['condition']})"
}
state_config['while'] = while_config
return self.execute_while_loop(state_config, context)
Loop Completion
def complete_loop(self, results, exit_reason, context):
"""Handle loop completion and determine next state."""
loop_stats = {
'total_iterations': len(results),
'successful': sum(1 for r in results if r.success),
'failed': sum(1 for r in results if not r.success),
'exit_reason': exit_reason,
'final_context': context.copy()
}
return LoopResult(
results=results,
stats=loop_stats,
exit_reason=exit_reason
)
Use Cases
1. Data Exfiltration with Pagination
states:
exfiltrate_users:
description: "Extract all user records"
variables:
page: 0
all_users: []
has_more: true
request: |
GET /api/users?page={{ page }}&size=100
Authorization: Bearer {{ admin_token }}
extract:
users:
type: jpath
pattern: "$.data"
has_more:
type: jpath
pattern: "$.has_more"
update:
page: "{{ page + 1 }}"
all_users: "{{ all_users + users }}"
while:
condition: "{{ has_more }}"
max_iterations: 1000
timeout_seconds: 600
logger:
on_thread_enter: |
[Page {{ page + 1 }}] Fetching users...
on_thread_leave: |
{% if loop.last %}
✅ Exfiltrated {{ all_users | length }} total users
{% endif %}
next:
- on_loop_complete:
goto: save_exfiltrated_data
2. Account Draining Attack
states:
drain_balance:
description: "Withdraw all funds from account"
variables:
total_withdrawn: 0
attempts: 0
request: |
POST /api/withdraw
Authorization: Bearer {{ victim_token }}
{"amount": 100, "to_account": "{{ attacker_account }}"}
extract:
balance:
type: jpath
pattern: "$.remaining_balance"
withdrawn:
type: jpath
pattern: "$.withdrawn_amount"
success:
type: jpath
pattern: "$.success"
update:
total_withdrawn: "{{ total_withdrawn + (withdrawn if success else 0) }}"
attempts: "{{ attempts + 1 }}"
while:
condition: "{{ balance > 0 and success }}"
max_iterations: 100
timeout_seconds: 60
logger:
on_thread_leave: |
[Attempt {{ attempts }}]
{% if success %}
✓ Withdrew {{ withdrawn }}. Remaining: {{ balance }}
{% else %}
✗ Withdrawal failed
{% endif %}
{% if loop.last %}
💰 Total withdrawn: {{ total_withdrawn }}
{% endif %}
next:
- when:
- condition: "{{ balance == 0 }}"
goto: account_fully_drained
- when:
- condition: "{{ not success }}"
goto: withdrawal_blocked
- on_timeout:
goto: partial_drain
3. Rate Limit Discovery
states:
find_rate_limit:
description: "Send requests until rate limited"
variables:
request_count: 0
rate_limited: false
request: |
GET /api/data
Authorization: Bearer {{ token }}
extract:
remaining:
type: jpath
pattern: "$.rate_limit.remaining"
update:
request_count: "{{ request_count + 1 }}"
rate_limited: "{{ status == 429 }}"
until:
condition: "{{ rate_limited }}"
max_iterations: 10000
timeout_seconds: 300
logger:
on_thread_enter: |
[Request {{ request_count + 1 }}] Remaining: {{ remaining }}
next:
- when:
- condition: "{{ rate_limited }}"
goto: rate_limit_found
- on_timeout:
goto: no_rate_limit_detected
4. Privilege Escalation Loop
states:
escalate_to_admin:
description: "Try elevation methods until admin"
variables:
methods: ["token_manipulation", "role_injection", "jwt_tampering"]
current_method: 0
is_admin: false
request: |
POST /api/elevate
Authorization: Bearer {{ token }}
{"method": "{{ methods[current_method] }}"}
extract:
role:
type: jpath
pattern: "$.user.role"
update:
current_method: "{{ current_method + 1 }}"
is_admin: "{{ role == 'admin' }}"
until:
condition: "{{ is_admin or current_method >= methods | length }}"
max_iterations: 10
next:
- when:
- condition: "{{ is_admin }}"
goto: admin_access_gained
- otherwise:
goto: escalation_failed
5. Polling for Async Job Completion
states:
wait_for_job:
description: "Poll job status until complete"
request: |
GET /api/jobs/{{ job_id }}/status
Authorization: Bearer {{ token }}
extract:
status:
type: jpath
pattern: "$.job.status"
progress:
type: jpath
pattern: "$.job.progress"
until:
condition: "{{ status in ['completed', 'failed'] }}"
check_interval_ms: 2000 # Poll every 2 seconds
max_iterations: 300 # 10 minutes max
timeout_seconds: 600
logger:
on_thread_enter: |
[{{ loop.counter }}] Job status: {{ status }} ({{ progress }}%)
next:
- when:
- condition: "{{ status == 'completed' }}"
goto: job_succeeded
- when:
- condition: "{{ status == 'failed' }}"
goto: job_failed
- on_timeout:
goto: job_timeout
Testing Requirements
Unit Tests
Integration Tests
Edge Cases
Performance Tests
Output Examples
Successful Drainage
======================================================================
WHILE LOOP: drain_balance
======================================================================
Condition: {{ balance > 0 and success }}
Max iterations: 100
Timeout: 60s
======================================================================
[Attempt 1] ✓ Withdrew 100. Remaining: 900
[Attempt 2] ✓ Withdrew 100. Remaining: 800
[Attempt 3] ✓ Withdrew 100. Remaining: 700
[Attempt 4] ✓ Withdrew 100. Remaining: 600
[Attempt 5] ✓ Withdrew 100. Remaining: 500
[Attempt 6] ✓ Withdrew 100. Remaining: 400
[Attempt 7] ✓ Withdrew 100. Remaining: 300
[Attempt 8] ✓ Withdrew 100. Remaining: 200
[Attempt 9] ✓ Withdrew 100. Remaining: 100
[Attempt 10] ✓ Withdrew 100. Remaining: 0
💰 Total withdrawn: 1000
======================================================================
LOOP RESULTS
======================================================================
Total iterations: 10
Exit reason: condition_false (balance == 0)
Successful: 10 (100%)
Failed: 0 (0%)
Duration: 4.53s
⚠ VULNERABLE: Successfully drained entire account!
======================================================================
Rate Limit Detected
======================================================================
UNTIL LOOP: find_rate_limit
======================================================================
Condition: {{ rate_limited }}
Max iterations: 10000
Timeout: 300s
======================================================================
[Request 1] Remaining: 999
[Request 2] Remaining: 998
[Request 3] Remaining: 997
...
[Request 998] Remaining: 2
[Request 999] Remaining: 1
[Request 1000] Remaining: 0
[Request 1001] Status: 429 - Rate Limited!
======================================================================
LOOP RESULTS
======================================================================
Total iterations: 1001
Exit reason: condition_true (rate_limited)
Duration: 45.2s
📊 Rate Limit Analysis:
Requests before limit: 1000
Time to rate limit: 45.2s
Average rate: 22.1 req/s
Limit type: Fixed window (1000 requests)
✓ Rate limit threshold identified
======================================================================
Documentation Updates
Schema Updates
{
"properties": {
"while": {
"type": "object",
"required": ["condition"],
"properties": {
"condition": {
"type": "string",
"description": "Jinja2 expression evaluated as boolean. Loop continues while true."
},
"max_iterations": {
"type": "integer",
"minimum": 1,
"maximum": 100000,
"default": 1000,
"description": "Maximum iterations before forced exit"
},
"timeout_seconds": {
"type": "integer",
"minimum": 1,
"maximum": 3600,
"default": 300,
"description": "Maximum execution time in seconds"
},
"check_interval_ms": {
"type": "integer",
"minimum": 0,
"maximum": 60000,
"default": 0,
"description": "Delay between iterations in milliseconds"
},
"stop_on_error": {
"type": "boolean",
"default": false,
"description": "Stop loop on first request error"
},
"min_iterations": {
"type": "integer",
"minimum": 1,
"default": 1,
"description": "Minimum iterations before checking condition"
}
}
},
"until": {
"type": "object",
"required": ["condition"],
"properties": {
"condition": {
"type": "string",
"description": "Jinja2 expression evaluated as boolean. Loop continues until true."
},
"max_iterations": {"type": "integer", "minimum": 1, "default": 1000},
"timeout_seconds": {"type": "integer", "minimum": 1, "default": 300},
"check_interval_ms": {"type": "integer", "minimum": 0, "default": 0},
"stop_on_error": {"type": "boolean", "default": false},
"min_iterations": {"type": "integer", "minimum": 1, "default": 1}
}
}
}
}
Next State Transitions
Support special transitions for loop exit reasons:
next:
- on_loop_complete:
goto: normal_exit
- on_timeout:
goto: timeout_handler
- on_max_iterations:
goto: limit_reached
- on_error:
goto: error_handler
Acceptance Criteria
Related Issues
- Depends on: #XX Loop Context Variables (needs loop.index, etc.)
- Blocks: #XX State Variables and Accumulators
- Related to: #XX Multi-Condition When Blocks (condition syntax)
- Follow-up: #XX Loop Safety Limits (detailed safety features)
While/Until Loop Implementation
Epic: #3
Phase: 2 - Advanced Loops (v0.2)
Priority: High
Estimated Effort: 3-4 days
Summary
Implement
whileanduntilloops that continue execution based on dynamic conditions evaluated at runtime, enabling adaptive attack patterns that respond to target behavior.Motivation
Fixed-iteration loops (
loop.iterations) are limited for scenarios where:Current limitation: Cannot model "keep trying until balance == 0" or "drain all pages of results".
Real-World Scenarios
Proposed Syntax
While Loop (Continue While True)
Until Loop (Continue Until True)
While vs Until
while conditionwhile not conditionExample Comparison
Loop Configuration Parameters
Required Parameters
condition(string)Jinja2 expression evaluated as boolean.
Supported expressions:
{{ x > 10 }},{{ role == 'admin' }}{{ a and b }},{{ x or y }},{{ not z }}{{ 'admin' in roles }}{{ balance | int > 0 }}{{ (balance > 0) and (attempts < 10) }}Optional Parameters
max_iterations(integer)Maximum number of iterations before forced exit (safety limit).
Default: 1000
Range: 1 to 100,000
timeout_seconds(integer)Maximum execution time for entire loop.
Default: 300 (5 minutes)
Range: 1 to 3600
check_interval_ms(integer)Delay between condition checks (useful for polling).
Default: 0 (no delay)
Range: 0 to 60000
stop_on_error(boolean)Stop loop if request fails.
Default: false
min_iterations(integer)Minimum iterations before condition is checked.
Default: 1
Implementation Details
Condition Evaluation
Loop Execution
Loop Completion
Use Cases
1. Data Exfiltration with Pagination
2. Account Draining Attack
3. Rate Limit Discovery
4. Privilege Escalation Loop
5. Polling for Async Job Completion
Testing Requirements
Unit Tests
max_iterationslimit enforcedtimeout_secondslimit enforcedmin_iterationsrespected before condition checkcheck_interval_msdelays correctlystop_on_errorstops on first errorIntegration Tests
Edge Cases
Performance Tests
Output Examples
Successful Drainage
Rate Limit Detected
Documentation Updates
Schema Updates
{ "properties": { "while": { "type": "object", "required": ["condition"], "properties": { "condition": { "type": "string", "description": "Jinja2 expression evaluated as boolean. Loop continues while true." }, "max_iterations": { "type": "integer", "minimum": 1, "maximum": 100000, "default": 1000, "description": "Maximum iterations before forced exit" }, "timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 3600, "default": 300, "description": "Maximum execution time in seconds" }, "check_interval_ms": { "type": "integer", "minimum": 0, "maximum": 60000, "default": 0, "description": "Delay between iterations in milliseconds" }, "stop_on_error": { "type": "boolean", "default": false, "description": "Stop loop on first request error" }, "min_iterations": { "type": "integer", "minimum": 1, "default": 1, "description": "Minimum iterations before checking condition" } } }, "until": { "type": "object", "required": ["condition"], "properties": { "condition": { "type": "string", "description": "Jinja2 expression evaluated as boolean. Loop continues until true." }, "max_iterations": {"type": "integer", "minimum": 1, "default": 1000}, "timeout_seconds": {"type": "integer", "minimum": 1, "default": 300}, "check_interval_ms": {"type": "integer", "minimum": 0, "default": 0}, "stop_on_error": {"type": "boolean", "default": false}, "min_iterations": {"type": "integer", "minimum": 1, "default": 1} } } } }Next State Transitions
Support special transitions for loop exit reasons:
Acceptance Criteria
whileloop executes correctly with boolean conditionsuntilloop executes correctly (inverse of while)Related Issues