# Regex Denial of Service (ReDoS) via Catastrophic Backtracking

Language: Python
Severity: High
CWE: CWE-1333

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
import re
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/iot/device/register', methods=['POST'])
def register_device():
    payload = request.json
    dev_serial = payload.get('serial_number', '')
    fw_version = payload.get('firmware_version', '')
    serial_pat = re.compile(r'^([A-Z0-9]+[A-Z])+[0-9]$')
    fw_pat = re.compile(r'^(v?(\d+\.)+){1,10}(\d+)$')
    if not serial_pat.match(dev_serial):
        return jsonify({'error': 'Invalid serial number format'}), 400
    if not fw_pat.match(fw_version):
        return jsonify({'error': 'Invalid firmware version format'}), 400
    return jsonify({'status': 'registered', 'serial': dev_serial, 'firmware': fw_version}), 200
```

## Explanation

The user-controlled inputs 'dev_serial' and 'fw_version' are extracted from the POST JSON body without sanitization and passed directly to regex patterns containing nested quantifiers. The pattern '^([A-Z0-9]+[A-Z])+[0-9]$' and '^(v?(\d+\.)+){1,10}(\d+)$' both suffer from catastrophic backtracking — when given a crafted input like 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!' or 'v1.1.1.1.1.1.1.1.1.1.1.!', the regex engine explores an exponential number of paths before failing, causing the server thread to hang indefinitely and resulting in a Denial of Service.

## Remediation

The fix eliminates catastrophic backtracking by replacing the two vulnerable regex patterns with equivalent patterns that avoid nested quantifiers. The serial pattern ^[A-Z0-9]{1,128}[A-Z][0-9]$ uses a flat character class instead of the nested ([A-Z0-9]+[A-Z])+ group. The firmware pattern ^v?\d{1,10}(?:\.\d{1,10}){0,9}$ replaces the nested (v?(\d+\.)+){1,10} with a non-capturing group that matches dot-separated numeric segments without nesting quantifiers inside each other. Additionally, input length limits and type checks are enforced before regex evaluation to provide defense-in-depth against oversized inputs.

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

app = Flask(__name__)

# Precompiled safe regex patterns (no nested quantifiers)
# Serial: one or more alphanumeric chars ending with a digit,
#         with at least one letter somewhere before the final digit.
SERIAL_PAT = re.compile(r'^[A-Z0-9]{1,128}[A-Z][0-9]$')
# Firmware: optional 'v', then 1-10 dot-separated numeric groups, e.g. v1.2.3
FW_PAT = re.compile(r'^v?\d{1,10}(?:\.\d{1,10}){0,9}$')

# Maximum allowed input lengths to prevent excessive processing
MAX_SERIAL_LEN = 256
MAX_FW_VERSION_LEN = 128


@app.route('/api/iot/device/register', methods=['POST'])
def register_device():
    payload = request.json
    if not payload:
        return jsonify({'error': 'Missing JSON payload'}), 400

    dev_serial = payload.get('serial_number', '')
    fw_version = payload.get('firmware_version', '')

    # Validate types
    if not isinstance(dev_serial, str) or not isinstance(fw_version, str):
        return jsonify({'error': 'Invalid input types'}), 400

    # Enforce length limits before any regex processing
    if len(dev_serial) > MAX_SERIAL_LEN:
        return jsonify({'error': 'Serial number too long'}), 400
    if len(fw_version) > MAX_FW_VERSION_LEN:
        return jsonify({'error': 'Firmware version too long'}), 400

    if not SERIAL_PAT.match(dev_serial):
        return jsonify({'error': 'Invalid serial number format'}), 400

    if not FW_PAT.match(fw_version):
        return jsonify({'error': 'Invalid firmware version format'}), 400

    return jsonify({'status': 'registered', 'serial': dev_serial, 'firmware': fw_version}), 200
```
