# Regex DoS via Catastrophic Backtracking in re.match()

Language: Python
Severity: High
CWE: CWE-1333

## Source
9

## Flow
9-12

## Sink
12

## Vulnerable Code
```python
import re
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/iot/device/validate', methods=['POST'])
def validate_device_serial():
    device_serial = request.json.get('serial_number', '')
    firmware_pattern = r'^(([A-Z0-9]+[-_]?)+)*DEVICE$'
    
    if re.match(firmware_pattern, device_serial):
        return jsonify({
            'status': 'valid',
            'message': 'Device serial authenticated',
            'provisioning': 'approved'
        }), 200
    else:
        return jsonify({
            'status': 'invalid',
            'message': 'Serial format rejected'
        }), 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

## Explanation

The regex pattern '^(([A-Z0-9]+[-_]?)+)*DEVICE$' contains nested quantifiers that cause catastrophic backtracking. When untrusted input from request.json is matched against this pattern using re.match(), an attacker can send specially crafted strings (e.g., repeating patterns without the 'DEVICE' suffix) that force exponential time complexity, leading to ReDoS and service unavailability.

## Remediation

The fix replaces the vulnerable regex pattern containing nested quantifiers `(([A-Z0-9]+[-_]?)+)*` with a safe equivalent `[A-Z0-9]+([\-_][A-Z0-9]+)*` that matches the same logical format but uses linear-time matching without ambiguous backtracking paths. Additionally, an input length limit of 128 characters is enforced to provide defense-in-depth against any remaining computational complexity.

## Secure Code
```python
import re
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/iot/device/validate', methods=['POST'])
def validate_device_serial():
    device_serial = request.json.get('serial_number', '')
    
    # Input length limit to prevent abuse
    if not device_serial or len(device_serial) > 128:
        return jsonify({
            'status': 'invalid',
            'message': 'Serial format rejected'
        }), 400
    
    # Safe regex without nested quantifiers
    # Matches one or more groups of [A-Z0-9]+ optionally followed by - or _, ending with DEVICE
    firmware_pattern = r'^[A-Z0-9]+([\-_][A-Z0-9]+)*DEVICE$'
    
    if re.match(firmware_pattern, device_serial):
        return jsonify({
            'status': 'valid',
            'message': 'Device serial authenticated',
            'provisioning': 'approved'
        }), 200
    else:
        return jsonify({
            'status': 'invalid',
            'message': 'Serial format rejected'
        }), 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```
