# Regex Denial of Service (ReDoS) via Catastrophic Backtracking

Language: Python
Severity: High
CWE: CWE-1333

## Source
6

## Flow
6-7-8

## Sink
8

## Vulnerable Code
```python
import re
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/iot/telemetry/validate', methods=['POST'])
def validate_sensor_payload():
    raw_data = request.json.get('sensor_reading', '')
    fmt_pattern = re.compile(r'^(\d+\.?\d*)+\s*(kg|lb|g|oz|mg)+$')
    match_result = fmt_pattern.match(raw_data)
    if not match_result:
        return jsonify({'status': 'invalid', 'msg': 'Sensor reading format rejected'}), 400
    return jsonify({'status': 'ok', 'parsed': match_result.group()}), 200
```

## Explanation

The untrusted input 'raw_data' is taken directly from user-controlled JSON (line 6) and matched against a regex pattern containing nested quantifiers '(\d+\.?\d*)+' (line 7). This pattern causes catastrophic backtracking when applied to crafted inputs like '1111111111111111111x', because the regex engine exhaustively tries all combinations of how the outer '+' and inner '+' can partition the digit sequence before failing, leading to exponential time complexity and server thread exhaustion.

## Remediation

The fix removes the nested quantifiers that caused catastrophic backtracking. The original pattern `(\d+\.?\d*)+` wrapped a repeating group inside another repetition; this has been replaced with the linear-time equivalent `\d+\.?\d*` (no outer group repetition). The unit suffix group's unnecessary `+` quantifier was also removed, and the unit alternatives are placed in a non-capturing group. Additionally, an input-length check and type validation are applied before regex matching to provide defense-in-depth against oversized payloads.

## Secure Code
```python
import re
from flask import Flask, request, jsonify
app = Flask(__name__)

# Precompiled regex without nested quantifiers to prevent catastrophic backtracking.
# Matches a numeric value (integer or decimal) followed by optional whitespace and a unit.
_SENSOR_READING_RE = re.compile(r'^\d+\.?\d*\s*(?:kg|lb|g|oz|mg)$')

# Maximum allowed length for a sensor reading string to bound processing time.
_MAX_READING_LEN = 64

@app.route('/api/iot/telemetry/validate', methods=['POST'])
def validate_sensor_payload():
    raw_data = request.json.get('sensor_reading', '')

    # Guard: reject excessively long inputs before any regex processing.
    if not isinstance(raw_data, str) or len(raw_data) > _MAX_READING_LEN:
        return jsonify({'status': 'invalid', 'msg': 'Sensor reading format rejected'}), 400

    match_result = _SENSOR_READING_RE.match(raw_data)
    if not match_result:
        return jsonify({'status': 'invalid', 'msg': 'Sensor reading format rejected'}), 400
    return jsonify({'status': 'ok', 'parsed': match_result.group()}), 200
```
