{"title":"Regex DoS via Catastrophic Backtracking in re.match()","language":"Python","severity":"High","cwe":"CWE-1333","source_lines":[9],"flow_lines":[9,12],"sink_lines":[12],"vulnerable_code":"import re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/iot/device/validate', methods=['POST'])\ndef validate_device_serial():\n    device_serial = request.json.get('serial_number', '')\n    firmware_pattern = r'^(([A-Z0-9]+[-_]?)+)*DEVICE$'\n    \n    if re.match(firmware_pattern, device_serial):\n        return jsonify({\n            'status': 'valid',\n            'message': 'Device serial authenticated',\n            'provisioning': 'approved'\n        }), 200\n    else:\n        return jsonify({\n            'status': 'invalid',\n            'message': 'Serial format rejected'\n        }), 400\n\nif __name__ == '__main__':\n    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":"import re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/iot/device/validate', methods=['POST'])\ndef validate_device_serial():\n    device_serial = request.json.get('serial_number', '')\n    \n    # Input length limit to prevent abuse\n    if not device_serial or len(device_serial) > 128:\n        return jsonify({\n            'status': 'invalid',\n            'message': 'Serial format rejected'\n        }), 400\n    \n    # Safe regex without nested quantifiers\n    # Matches one or more groups of [A-Z0-9]+ optionally followed by - or _, ending with DEVICE\n    firmware_pattern = r'^[A-Z0-9]+([\\-_][A-Z0-9]+)*DEVICE$'\n    \n    if re.match(firmware_pattern, device_serial):\n        return jsonify({\n            'status': 'valid',\n            'message': 'Device serial authenticated',\n            'provisioning': 'approved'\n        }), 200\n    else:\n        return jsonify({\n            'status': 'invalid',\n            'message': 'Serial format rejected'\n        }), 400\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5000)"}