Skip to content

While/Until Loop Implementation #8

@maycon

Description

@maycon

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

  1. Data Exfiltration: Paginate through all records until no more pages
  2. Account Draining: Keep withdrawing funds until balance reaches zero
  3. Rate Limit Discovery: Send requests until rate limited
  4. Privilege Escalation: Try elevation until admin access granted
  5. 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

  • While loop executes while condition is true
  • While loop stops when condition becomes false
  • Until loop executes until condition is true
  • Until loop stops when condition becomes true
  • max_iterations limit enforced
  • timeout_seconds limit enforced
  • min_iterations respected before condition check
  • check_interval_ms delays correctly
  • stop_on_error stops on first error
  • Condition evaluation with complex expressions
  • Context updates between iterations
  • Loop variables available in condition

Integration Tests

  • Real HTTP requests in while loop
  • Extracted values used in condition
  • Variables update correctly across iterations
  • Multiple nested conditionals work
  • Transition to correct next state based on exit reason

Edge Cases

  • Condition never true (while exits immediately)
  • Condition never false (until exits immediately)
  • Condition always true (hits max_iterations)
  • Zero iterations (condition false on first check)
  • Timeout during first iteration
  • Invalid condition expression (clear error)
  • Extracted variable not available in context
  • Condition references non-existent variable

Performance Tests

  • 1000 iterations complete in reasonable time
  • Memory usage stable across iterations
  • No memory leaks in long loops
  • Condition evaluation overhead < 0.1ms
  • Context updates don't degrade over time

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

  • Add "While/Until Loops" section to README
  • Create comparison table: for vs while vs until
  • Add 5+ practical examples
  • Document condition expression syntax
  • Best practices for safety limits
  • Common pitfalls and solutions
  • Performance considerations

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

  • while loop executes correctly with boolean conditions
  • until loop executes correctly (inverse of while)
  • All safety limits enforced (max_iterations, timeout)
  • Condition evaluated with full Jinja2 support
  • Context updates between iterations
  • Loop variables available in conditions
  • Exit reason correctly determined and reported
  • Special transitions (on_timeout, etc.) work
  • Performance: <0.1ms overhead per iteration
  • Clear error messages for invalid conditions
  • Documentation with 5+ examples
  • All tests pass

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions