{"title":"Argument Injection via subprocess.run() with Untrusted Option Prefixes","language":"Python","severity":"Critical","cwe":"CWE-88","source_lines":[13,14],"flow_lines":[14,5,7,9],"sink_lines":[9],"vulnerable_code":"import subprocess\n\ndef execute_cloud_backup(bucket_name, sync_flags):\n    aws_region = \"us-east-1\"\n    cmd = [\"aws\", \"s3\", \"sync\", \"/data/backups\", f\"s3://{bucket_name}\"]\n    if sync_flags:\n        cmd.extend(sync_flags.split())\n    cmd.extend([\"--region\", aws_region])\n    result = subprocess.run(cmd, capture_output=True, text=True)\n    return result.stdout\n\nuser_bucket = request.form.get(\"bucket\")\nuser_options = request.form.get(\"options\")\nexecute_cloud_backup(user_bucket, user_options)","explanation":"Untrusted user input from 'user_options' is passed to execute_cloud_backup() and split directly into command arguments via cmd.extend(sync_flags.split()). This allows attackers to inject arbitrary AWS CLI flags like '--endpoint-url' to exfiltrate data or '--profile' to use unauthorized credentials, bypassing intended command structure.","remediation":"The fix implements strict allowlist validation for both the bucket name and sync flags. Only pre-approved, safe AWS CLI flags are permitted through the ALLOWED_FLAGS set, and flags that accept values are further constrained to known-safe options via ALLOWED_FLAG_VALUES. This prevents injection of dangerous flags like '--endpoint-url', '--profile', or '--include' that could be used to exfiltrate data or escalate privileges.","secure_code":"import subprocess\nimport re\n\n# Allowlist of safe AWS S3 sync flags that users can specify\nALLOWED_FLAGS = {\n    \"--delete\",\n    \"--exact-timestamps\",\n    \"--no-progress\",\n    \"--only-show-errors\",\n    \"--quiet\",\n    \"--dryrun\",\n    \"--no-follow-symlinks\",\n    \"--sse\",\n    \"--storage-class\",\n    \"--acl\"\n}\n\n# Allowed values for flags that accept parameters\nALLOWED_FLAG_VALUES = {\n    \"--storage-class\": [\"STANDARD\", \"REDUCED_REDUNDANCY\", \"STANDARD_IA\", \"ONEZONE_IA\", \"INTELLIGENT_TIERING\", \"GLACIER\", \"DEEP_ARCHIVE\"],\n    \"--sse\": [\"AES256\", \"aws:kms\"],\n    \"--acl\": [\"private\", \"public-read\", \"public-read-write\", \"authenticated-read\", \"bucket-owner-read\", \"bucket-owner-full-control\"]\n}\n\n# Allowlist pattern for bucket names (alphanumeric, hyphens, dots)\nBUCKET_NAME_PATTERN = re.compile(r'^[a-z0-9][a-z0-9.\\-]{1,61}[a-z0-9]$')\n\n\ndef validate_bucket_name(bucket_name):\n    if not bucket_name or not BUCKET_NAME_PATTERN.match(bucket_name):\n        raise ValueError(f\"Invalid bucket name: {bucket_name}\")\n    return bucket_name\n\n\ndef validate_sync_flags(sync_flags):\n    \"\"\"Parse and validate sync flags against an allowlist.\"\"\"\n    if not sync_flags:\n        return []\n\n    validated_args = []\n    tokens = sync_flags.split()\n    i = 0\n    while i < len(tokens):\n        token = tokens[i]\n        if token not in ALLOWED_FLAGS:\n            raise ValueError(f\"Disallowed flag: {token}\")\n        \n        if token in ALLOWED_FLAG_VALUES:\n            # This flag requires a value parameter\n            if i + 1 >= len(tokens):\n                raise ValueError(f\"Flag {token} requires a value\")\n            value = tokens[i + 1]\n            if value not in ALLOWED_FLAG_VALUES[token]:\n                raise ValueError(f\"Disallowed value '{value}' for flag {token}\")\n            validated_args.extend([token, value])\n            i += 2\n        else:\n            # Boolean flag, no value needed\n            validated_args.append(token)\n            i += 1\n\n    return validated_args\n\n\ndef execute_cloud_backup(bucket_name, sync_flags):\n    aws_region = \"us-east-1\"\n    \n    validated_bucket = validate_bucket_name(bucket_name)\n    validated_flags = validate_sync_flags(sync_flags)\n    \n    cmd = [\"aws\", \"s3\", \"sync\", \"/data/backups\", f\"s3://{validated_bucket}\"]\n    if validated_flags:\n        cmd.extend(validated_flags)\n    cmd.extend([\"--region\", aws_region])\n    \n    result = subprocess.run(cmd, capture_output=True, text=True)\n    return result.stdout\n\n\nuser_bucket = request.form.get(\"bucket\")\nuser_options = request.form.get(\"options\")\ntry:\n    execute_cloud_backup(user_bucket, user_options)\nexcept ValueError as e:\n    # Handle invalid input gracefully\n    abort(400, description=str(e))"}