{"title":"Regex Denial of Service (ReDoS) via Catastrophic Backtracking","language":"Python","severity":"High","cwe":"CWE-1333","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"import re\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\n@app.route('/api/iot/device/register', methods=['POST'])\ndef register_device():\n    payload = request.json\n    dev_serial = payload.get('serial_number', '')\n    fw_version = payload.get('firmware_version', '')\n    serial_pat = re.compile(r'^([A-Z0-9]+[A-Z])+[0-9]$')\n    fw_pat = re.compile(r'^(v?(\\d+\\.)+){1,10}(\\d+)$')\n    if not serial_pat.match(dev_serial):\n        return jsonify({'error': 'Invalid serial number format'}), 400\n    if not fw_pat.match(fw_version):\n        return jsonify({'error': 'Invalid firmware version format'}), 400\n    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":"import re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n# Precompiled safe regex patterns (no nested quantifiers)\n# Serial: one or more alphanumeric chars ending with a digit,\n#         with at least one letter somewhere before the final digit.\nSERIAL_PAT = re.compile(r'^[A-Z0-9]{1,128}[A-Z][0-9]$')\n# Firmware: optional 'v', then 1-10 dot-separated numeric groups, e.g. v1.2.3\nFW_PAT = re.compile(r'^v?\\d{1,10}(?:\\.\\d{1,10}){0,9}$')\n\n# Maximum allowed input lengths to prevent excessive processing\nMAX_SERIAL_LEN = 256\nMAX_FW_VERSION_LEN = 128\n\n\n@app.route('/api/iot/device/register', methods=['POST'])\ndef register_device():\n    payload = request.json\n    if not payload:\n        return jsonify({'error': 'Missing JSON payload'}), 400\n\n    dev_serial = payload.get('serial_number', '')\n    fw_version = payload.get('firmware_version', '')\n\n    # Validate types\n    if not isinstance(dev_serial, str) or not isinstance(fw_version, str):\n        return jsonify({'error': 'Invalid input types'}), 400\n\n    # Enforce length limits before any regex processing\n    if len(dev_serial) > MAX_SERIAL_LEN:\n        return jsonify({'error': 'Serial number too long'}), 400\n    if len(fw_version) > MAX_FW_VERSION_LEN:\n        return jsonify({'error': 'Firmware version too long'}), 400\n\n    if not SERIAL_PAT.match(dev_serial):\n        return jsonify({'error': 'Invalid serial number format'}), 400\n\n    if not FW_PAT.match(fw_version):\n        return jsonify({'error': 'Invalid firmware version format'}), 400\n\n    return jsonify({'status': 'registered', 'serial': dev_serial, 'firmware': fw_version}), 200"}