# PYTHON-LAMBDA-SEC-005: Lambda Command Injection via asyncio.create_subprocess_exec()

> **Severity:** CRITICAL | **CWE:** CWE-78 | **OWASP:** A03:2021

- **Language:** Python
- **Category:** AWS Lambda
- **URL:** https://codepathfinder.dev/registry/python/aws_lambda/PYTHON-LAMBDA-SEC-005
- **Detection:** `pathfinder scan --ruleset python/PYTHON-LAMBDA-SEC-005 --project .`

## Description

This rule detects command injection vulnerabilities in AWS Lambda functions where
untrusted event data flows into asyncio.create_subprocess_exec() calls within async
Lambda handlers.

asyncio.create_subprocess_exec() does not invoke a shell and passes arguments
directly to the process via execve(), making it the safer async subprocess API.
However, it is still vulnerable to argument injection when event data controls the
executable path (first argument) or argument values that the target binary interprets
as flags or commands. Classic examples include passing attacker-controlled strings as
filenames to tools like rsync, wget, or curl that interpret certain argument patterns
as additional operations.

Lambda functions receive input from the event dictionary populated by API Gateway,
SQS, SNS, S3, DynamoDB Streams, and other triggers. Fields such as event.get("body"),
event.get("queryStringParameters"), and event["Records"] are fully attacker-
controllable in public-facing deployments.

While the shell injection risk is reduced compared to create_subprocess_shell(),
the risk of argument injection and executable path manipulation remains when event
data is used in any argument position without validation.


## Vulnerable Code

```python
import os
import subprocess
import asyncio

# SEC-005: asyncio exec
async def handler_asyncio_exec(event, context):
    prog = event.get('program')
    proc = await asyncio.create_subprocess_exec(prog)
    return {"statusCode": 200}
```

## Secure Code

```python
import asyncio
import re
import json

ALLOWED_EXECUTABLES = {
    'convert': '/usr/bin/convert',
    'ffmpeg': '/opt/bin/ffmpeg',
}

async def lambda_handler(event, context):
    body = json.loads(event.get('body', '{}'))
    tool = body.get('tool', '')
    input_file = body.get('input', '')

    # SECURE: Validate executable against a hardcoded allowlist with full paths
    if tool not in ALLOWED_EXECUTABLES:
        return {'statusCode': 400, 'body': 'Unknown tool'}

    # SECURE: Validate filename with strict regex
    if not re.match(r'^[a-zA-Z0-9_.\-]+$', input_file):
        return {'statusCode': 400, 'body': 'Invalid filename'}

    # SECURE: Use create_subprocess_exec with validated, prefixed arguments
    process = await asyncio.create_subprocess_exec(
        ALLOWED_EXECUTABLES[tool], '--', f'/tmp/{input_file}',
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await process.communicate()
    return {'statusCode': 200, 'body': stdout.decode()}

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class AsyncioModule(QueryType):
    fqns = ["asyncio"]

# Lambda event sources — event dict is the primary untrusted input
_LAMBDA_SOURCES = [
    calls("event.get"),
    calls("event.items"),
    calls("event.values"),
    calls("event.keys"),
    calls("*.get"),
]


@python_rule(
    id="PYTHON-LAMBDA-SEC-005",
    name="Lambda Command Injection via asyncio exec",
    severity="CRITICAL",
    category="aws_lambda",
    cwe="CWE-78",
    tags="python,aws,lambda,command-injection,asyncio,OWASP-A03,CWE-78",
    message="Lambda event data flows to asyncio.create_subprocess_exec().",
    owasp="A03:2021",
)
def detect_lambda_asyncio_exec():
    """Detects Lambda event data flowing to asyncio exec subprocess."""
    return flows(
        from_sources=_LAMBDA_SOURCES,
        to_sinks=[
            AsyncioModule.method("create_subprocess_exec"),
        ],
        sanitized_by=[
            calls("shlex.quote"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Validate the executable argument against a hardcoded allowlist of absolute paths; never derive the executable from event data.
- Prefix user-controlled filename arguments with '--' to prevent the target binary from interpreting them as flags.
- Validate all event fields with strict allowlists or regular expressions before they appear in any argument position.
- Apply least-privilege IAM policies to the Lambda execution role to minimize the AWS APIs accessible if exploitation occurs.
- Prefer native Python libraries over subprocess calls wherever possible to eliminate the subprocess attack surface.

## Security Implications

- **Executable Path Hijacking:** If event data controls the first argument (the executable path) of
create_subprocess_exec(), an attacker can point the execution to any binary
accessible in the Lambda filesystem, including user-written scripts in /tmp.
Always use hardcoded, absolute executable paths from an allowlist.

- **Argument Injection via Flag-Like Values:** Many command-line tools interpret arguments beginning with '-' as flags.
If event data is passed as an argument without the '--' separator, values like
'--exec', '--output', or '-rf' may activate unintended behaviors in the target
binary. Prefix user-provided arguments with '--' when the target tool supports it.

- **AWS Credential Access via Argument Injection:** Argument injection can cause tools like curl, wget, or rsync to write output
files, follow redirects, or establish connections in ways that expose the
Lambda execution environment's AWS credentials stored in environment variables.

- **/tmp Exfiltration and Modification:** Argument injection into file processing tools can redirect output to attacker-
controlled destinations, read arbitrary /tmp files, or overwrite Lambda
deployment package files if /tmp is used for code storage.


## FAQ

**Q: If create_subprocess_exec() doesn't invoke a shell, why is it still dangerous?**

create_subprocess_exec() bypasses the shell, so classic shell injection with
semicolons, pipes, and $() does not work. However, argument injection remains
a risk: if event data controls the executable path, it can point to any binary
in the Lambda filesystem. If event data controls argument values, many tools
interpret arguments starting with '-' as flags, potentially activating unintended
behaviors. Always validate executable paths and argument values from allowlists.


**Q: How does this rule differ from SEC-004 which covers create_subprocess_shell()?**

SEC-004 covers create_subprocess_shell(), which always invokes the shell and is
vulnerable to shell metacharacter injection. SEC-005 covers create_subprocess_exec(),
which does not invoke a shell but is still vulnerable to argument injection when
event data controls argument positions. Both rules are needed because developers
sometimes switch from shell to exec thinking it eliminates all injection risk,
when argument injection remains possible without input validation.


**Q: What is the '--' argument separator and when should I use it?**

The '--' argument is a POSIX convention that signals the end of flag arguments
to most command-line tools. Arguments after '--' are treated as positional
arguments (filenames, etc.) rather than flags. Prefixing user-provided filenames
with '--' prevents the tool from interpreting values like '--delete' or '-rf' as
flags. Use '--' before any argument derived from event data in all subprocess calls.


**Q: Can I use event data as the executable path if it's validated with a regex?**

No. Even a strict regex validation on an executable path does not guarantee safety,
because an attacker may craft a path that passes the regex but still points to an
unintended binary. Always use a hardcoded dictionary or set of allowed executables
with their full absolute paths, and use the event data only to select a key from
that allowlist, never to construct the path itself.


**Q: How should I handle Lambda functions that use asyncio to process SQS batches?**

For SQS batch processing, iterate over event["Records"] and process each record
in separate async tasks. Validate each record's fields individually before use in
subprocess calls. Implement SQS batch item failure reporting so that a single
malformed record that fails validation does not cause the entire batch to be retried.


## References

- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
- [Python asyncio.create_subprocess_exec documentation](https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.create_subprocess_exec)
- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [AWS Lambda Security Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html)
- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
- [Python asyncio subprocess documentation](https://docs.python.org/3/library/asyncio-subprocess.html)

---

Source: https://codepathfinder.dev/registry/python/aws_lambda/PYTHON-LAMBDA-SEC-005
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
