{"title":"Regex DoS via Catastrophic Backtracking in re.search()","language":"Python","severity":"High","cwe":"CWE-1333","source_lines":[5],"flow_lines":[5,6],"sink_lines":[6],"vulnerable_code":"import re\nfrom flask import request, jsonify\n\ndef validate_iot_device_serial(serial_number):\n    pattern = r'^(IOT-([A-Z0-9]+-)*)+-[A-Z]{2}$'\n    if re.search(pattern, serial_number):\n        return jsonify({\"status\": \"valid\", \"device_id\": serial_number})\n    return jsonify({\"status\": \"invalid\"})\n\n@app.route('/api/v2/device/register', methods=['POST'])\ndef register_iot_device():\n    device_serial = request.json.get('serial_number', '')\n    validation_result = validate_iot_device_serial(device_serial)\n    return validation_result","explanation":"The regex pattern `^(IOT-([A-Z0-9]+-)*)+-[A-Z]{2}$` contains nested quantifiers: the inner group `([A-Z0-9]+-)` uses `*` and the outer group uses `+`, creating ambiguous overlap. When a malformed serial number with many repeated segments is provided (e.g., 'IOT-A-B-C-...-Z-'), the regex engine explores an exponential number of matching combinations attempting to satisfy the pattern, causing CPU exhaustion and denial of service. An unauthenticated attacker can exploit this endpoint to render the IoT registration service unavailable.","remediation":"The fix replaces the nested quantifier structure with a flat, unambiguous pattern `^IOT-[A-Z0-9]+(?:-[A-Z0-9]+)*-[A-Z]{2}$` that matches the intended serial format (e.g., IOT-ABC123-DEF456-US) without any ambiguous nesting. An input length limit (MAX_SERIAL_LENGTH = 128) is enforced before regex evaluation to prevent excessively long inputs from reaching the engine. `re.fullmatch` is used instead of `re.search` to ensure the entire string is validated, eliminating partial match risks. Together these changes ensure linear-time regex evaluation regardless of input content.","secure_code":"import re\nfrom flask import request, jsonify\n\nMAX_SERIAL_LENGTH = 128\n\ndef validate_iot_device_serial(serial_number):\n    # Length check to prevent excessively long inputs\n    if not serial_number or len(serial_number) > MAX_SERIAL_LENGTH:\n        return jsonify({\"status\": \"invalid\"})\n\n    # Use a non-backtracking pattern that avoids nested quantifiers\n    # Expected format: IOT- followed by one or more alphanumeric segments separated by hyphens,\n    # ending with a two-letter country/region code\n    # e.g., IOT-ABC123-DEF456-US\n    pattern = r'^IOT-[A-Z0-9]+(?:-[A-Z0-9]+)*-[A-Z]{2}$'\n\n    # Use re.fullmatch to anchor the match and avoid partial matching issues\n    if re.fullmatch(pattern, serial_number):\n        return jsonify({\"status\": \"valid\", \"device_id\": serial_number})\n    return jsonify({\"status\": \"invalid\"})\n\n@app.route('/api/v2/device/register', methods=['POST'])\ndef register_iot_device():\n    device_serial = request.json.get('serial_number', '')\n    validation_result = validate_iot_device_serial(device_serial)\n    return validation_result"}