# PYTHON-LAMBDA-SEC-004: Lambda Command Injection via asyncio.create_subprocess_shell()

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

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

## Description

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

Lambda functions increasingly use async/await patterns with asyncio for concurrent
I/O operations. asyncio.create_subprocess_shell() is the async equivalent of
subprocess.run(shell=True) and carries the same shell injection risk: the command
string is passed to /bin/sh for interpretation, and any shell metacharacters in the
event data will be executed as shell syntax.

Lambda functions receive input from the event dictionary populated by API Gateway,
SQS, SNS, S3, DynamoDB Streams, and other triggers. Fields like event.get("body"),
event.get("queryStringParameters"), and event["Records"] are attacker-controllable.
There is no sanitization layer between the event payload and application code.

The async nature of the handler does not provide any additional security for shell
commands. Injected commands run asynchronously but still complete within the
invocation timeout, allowing exfiltration of the Lambda execution role's AWS
credentials (available in environment variables) and other sensitive data before
the invocation ends.


## Vulnerable Code

```python
import os
import subprocess
import asyncio

# SEC-004: asyncio shell
async def handler_asyncio_shell(event, context):
    cmd = event.get('cmd')
    proc = await asyncio.create_subprocess_shell(cmd)
    return {"statusCode": 200}
```

## Secure Code

```python
import asyncio
import re
import json

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

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

    # SECURE: Use create_subprocess_exec with individual arguments — no shell
    process = await asyncio.create_subprocess_exec(
        'convert', '--', f'/tmp/{filename}', f'/tmp/out_{filename}',
        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-004",
    name="Lambda Command Injection via asyncio shell",
    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_shell().",
    owasp="A03:2021",
)
def detect_lambda_asyncio_shell():
    """Detects Lambda event data flowing to asyncio shell subprocess."""
    return flows(
        from_sources=_LAMBDA_SOURCES,
        to_sinks=[
            AsyncioModule.method("create_subprocess_shell"),
        ],
        sanitized_by=[
            calls("shlex.quote"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Replace asyncio.create_subprocess_shell() with asyncio.create_subprocess_exec() using individual arguments, which bypasses the shell entirely.
- Never interpolate Lambda event data into shell command strings, even with asyncio async patterns.
- Validate all event fields with strict allowlists or regular expressions before they appear in any subprocess or asyncio subprocess argument.
- Apply least-privilege IAM policies to the Lambda execution role to limit the AWS APIs accessible if credentials are exfiltrated.
- Enable AWS CloudTrail and VPC Flow Logs to detect unusual outbound connections from the Lambda execution environment.

## Security Implications

- **Shell Injection in Async Lambda Handlers:** asyncio.create_subprocess_shell() passes the command string to /bin/sh for
execution. Lambda event data embedded via f-strings or concatenation allows
an attacker to inject shell metacharacters (;, |, $(), ``) that chain additional
commands. The async execution model does not prevent injection; commands run
concurrently and complete before the handler returns.

- **AWS Credential Exfiltration via Async Shell:** The Lambda execution environment exposes AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
and AWS_SESSION_TOKEN as environment variables. An async shell injection can
spawn a background process that exfiltrates these credentials via curl or wget
to attacker-controlled infrastructure, completing before the Lambda invocation ends.

- **Concurrent Exploitation Amplification:** Async handlers may process multiple event records concurrently (e.g., SQS batch
processing). A batch of malicious SQS messages could trigger multiple concurrent
injections within a single invocation, amplifying the attack surface and
potentially overwhelming logging or detection systems.

- **VPC Lateral Movement:** Lambda functions in a VPC have network access to internal resources. An async
shell injection can initiate non-blocking outbound connections to internal
endpoints (RDS, ElastiCache, internal APIs) that are not accessible from the
internet, enabling lateral movement through the VPC.


## FAQ

**Q: How is asyncio.create_subprocess_shell() different from asyncio.create_subprocess_exec()?**

create_subprocess_shell() passes the command to the system shell (/bin/sh) for
interpretation, exactly like subprocess.run(shell=True). This means shell
metacharacters in user input are interpreted as shell syntax. create_subprocess_exec()
takes individual arguments and passes them directly to execve() without shell
interpretation, making it safe from shell injection when arguments are validated.


**Q: Does the async nature of the Lambda handler prevent or limit injection?**

No. The async execution model allows non-blocking I/O but does not impose any
security boundary on the shell commands that are spawned. The injected shell
command runs as a subprocess of the Lambda process and completes before the
invocation ends. Async concurrency may actually amplify the attack if multiple
event records are processed concurrently and each contains an injection payload.


**Q: What is the correct async replacement for asyncio.create_subprocess_shell()?**

Use asyncio.create_subprocess_exec() with individual string arguments for each
part of the command. This function bypasses the shell entirely and passes
arguments directly to the process. Validate all arguments from event data before
use and source executable names from a hardcoded allowlist rather than event data.


**Q: Can this rule detect injection in async helper functions called by the Lambda handler?**

Yes. The rule performs inter-procedural analysis and follows taint from the
Lambda handler's event parameter through await calls, async function arguments,
and return values. If event data flows from lambda_handler() through an async
helper to asyncio.create_subprocess_shell() in another module, the finding
is still reported.


**Q: What if SQS triggers deliver multiple records and one is malicious?**

SQS batch processing often loops over event["Records"] and processes each record.
If the loop body passes record data to create_subprocess_shell(), each record in
the batch is a potential injection vector. Validate every record field individually
before use. Use batch item failure reporting to handle individual malformed records
without failing the entire batch.


## References

- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
- [Python asyncio.create_subprocess_shell documentation](https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.create_subprocess_shell)
- [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-004
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
