{"title":"Command Injection via Shell Metacharacter Expansion","language":"Python","severity":"Critical","cwe":"CWE-78","source_lines":[6,7],"flow_lines":[6,7,10,11],"sink_lines":[11],"vulnerable_code":"import subprocess\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\n@app.route('/api/v1/network/latency', methods=['GET'])\ndef measure_latency():\n    target_node = request.args.get('node', '')\n    pkt_count = request.args.get('packets', '4')\n    if not target_node:\n        return jsonify({'error': 'node parameter required'}), 400\n    diag_cmd = f\"ping -c {pkt_count} {target_node}\"\n    result = subprocess.run(diag_cmd, shell=True, capture_output=True, text=True, timeout=15)\n    return jsonify({'stdout': result.stdout, 'stderr': result.stderr, 'exit_code': result.returncode})","explanation":"Both 'target_node' and 'pkt_count' are taken directly from user-controlled HTTP query parameters without any sanitization or validation. They are interpolated into a shell command string via f-string on line 10, then executed with shell=True on line 11, which instructs the OS shell to interpret metacharacters — enabling arbitrary command injection.","remediation":"The fix eliminates the command injection vulnerability through three changes: (1) strict input validation using allowlist regex patterns ensures target_node can only be a valid hostname or IP address and pkt_count must be a bounded integer, rejecting any shell metacharacters; (2) the command is passed as a list of arguments instead of an f-string, and (3) shell=True is removed (defaulting to shell=False), so the OS never interprets the arguments through a shell, preventing metacharacter expansion even if validation were bypassed.","secure_code":"import subprocess\nimport re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n\ndef is_valid_hostname_or_ip(value):\n    \"\"\"Validate that the value is a legitimate hostname or IP address.\"\"\"\n    # Match IPv4 addresses\n    ipv4_pattern = r'^(\\d{1,3}\\.){3}\\d{1,3}$'\n    # Match IPv6 addresses (simplified)\n    ipv6_pattern = r'^[0-9a-fA-F:]+$'\n    # Match valid hostnames (RFC 1123): alphanumeric, hyphens, dots\n    hostname_pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*$'\n\n    if re.match(ipv4_pattern, value):\n        # Additionally validate each octet is 0-255\n        parts = value.split('.')\n        return all(0 <= int(p) <= 255 for p in parts)\n    if re.match(ipv6_pattern, value) and len(value) <= 45:\n        return True\n    if re.match(hostname_pattern, value) and len(value) <= 253:\n        return True\n    return False\n\n\ndef is_valid_packet_count(value):\n    \"\"\"Validate that the packet count is a reasonable positive integer.\"\"\"\n    try:\n        count = int(value)\n        return 1 <= count <= 100\n    except (ValueError, TypeError):\n        return False\n\n\n@app.route('/api/v1/network/latency', methods=['GET'])\ndef measure_latency():\n    target_node = request.args.get('node', '')\n    pkt_count = request.args.get('packets', '4')\n\n    if not target_node:\n        return jsonify({'error': 'node parameter required'}), 400\n\n    if not is_valid_hostname_or_ip(target_node):\n        return jsonify({'error': 'Invalid node: must be a valid hostname or IP address'}), 400\n\n    if not is_valid_packet_count(pkt_count):\n        return jsonify({'error': 'Invalid packets: must be an integer between 1 and 100'}), 400\n\n    # Use argument list (shell=False) to prevent shell metacharacter interpretation\n    result = subprocess.run(\n        ['ping', '-c', str(int(pkt_count)), target_node],\n        capture_output=True, text=True, timeout=15\n    )\n    return jsonify({'stdout': result.stdout, 'stderr': result.stderr, 'exit_code': result.returncode})"}