Breaking the AWS MCP Python Sandbox A security researcher bypassed the Python restrictions in AWS's managed MCP Server, reaching system command execution inside the AWS Lambda sandbox that runs the `aws___run_script` tool, which AWS made generally available on May 6, 2026. The researcher, working with Claude Opus 4.6, built a Python CLI MCP client using `mcp-proxy-for-aws` and a limited-permission test IAM user to probe the tool, then defeated both of its protection layers: an AST-based static validator and a runtime layer that swaps builtins for restricted proxies such as `_safe_type()` and limits `open()`. The bypass matters because the tool executes agent-supplied Python inside AWS's environment using the user's IAM credentials. Preface preface AWS announced the general availability of the AWS MCP Server https://aws.amazon.com/blogs/aws/the-aws-mcp-server-is-now-generally-available/ on May 6, 2026. It is a managed remote Model Context Protocol MCP server. An MCP server exposes certain tools/functions and lets your AI agent call them. It extends your AI agent’s capabilities. The AWS MCP Server makes this possible for coding assistants such as Claude Code, Kiro and Cursor to retrieve current AWS documentation, make AWS API calls using the IAM credentials supplied by the user, and provide guidance through skills. Those tools can be used for tasks such as retrieving account information or creating an EC2 instance. AWS’s MCP server provides a tool called aws run script . This tool accepts a short Python script as an argument and runs it in a server-side sandbox. Its purpose is to let an agent combine multiple AWS API calls, filter the responses and process the result in one request programatically, instead of making several separate calls. The idea of user-supplied Python code executing inside an AWS managed sandboxed environment is interesting aws run script runs the supplied code in an AWS Lambda sandbox. The sandbox restricts imports, builtins, attribute access and file operations. In this blog, I explain how I bypassed those Python restrictions and reached system command execution inside the Lambda environment. I performed this research collaboratively with Claude Opus 4.6. Discovery discovery Normally connecting the MCP server to Cursor or any other agent adds friction for security testing because your payloads have to pass through the AI agent which then invokes the tools. While it is fine for a while, but later it becomes a pain when your payloads starts become overly chaotic. So I quickly created a Python CLI as an MCP client that could let me enumerate the server’s tools and invoke them. The client used mcp-proxy-for-aws https://github.com/aws/mcp-proxy-for-aws , which signed requests with my local AWS credentials. I created a test IAM user in my AWS account with limited permissions for this assessment. This made it possible to send controlled inputs to each tool and examine the returned results. Although the Python code ran inside the Lambda, dangerous Python code was immediately blocked due to the custom sandbox implemented, to prevent code execution inside the Lambda runtime. I wanted to find out what actually prevented it from reaching the process, filesystem or environment beneath the Python sandbox. Understanding the Sandbox understanding-the-sandbox The submitted code passed through two layers of protection. Firstly, a static validator inspected the code’s Abstract Syntax Tree AST . An AST is a structured representation of source code. ASTs are used widely in compilers, code interpreters, linters, etc. An AST is a data structure containing the structural logic of the code such as imports, calls, attribute reads and assignments. The validator in the sandbox used AST to parse the Python script passed into aws run script to reject certain malicious looking operations before the code gets executed. Secondly, a runtime layer replaced builtins with restricted implementations or proxy objects. In this context, a proxy is a replacement object that acts like the original object but limits how it can be used. For example, instead of the real type , the script gets safe type which returns restricted proxy classes that block access to sensitive attributes. The sandbox also limited open to control file access. For example, reading sensitive attributes through obj. class was blocked. The getattr builtin was also blocked, as was importing operator to call operator.attrgetter . getattr is simply another way to read an attribute. In normal Python, two lines produce the same output: python from types import SimpleNamespace person = SimpleNamespace name="John Doe", age=900 print person.name John Doe print getattr person, "name" John Doe They both read name from person . When the attribute name is determined at runtime, getattr is useful, as the attribute name can be passed as a string. The sandbox covered both of these common forms of attribute access. It did not account for structural pattern matching. The Flaw: Attribute Reads Hidden in a Pattern the-flaw-attribute-reads-hidden-in-a-pattern Python 3.10 introduced structural pattern matching through match/case . It checks whether a value fits a pattern and can bind parts of that value to names. A class pattern first checks an object’s type, then reads attributes from it. Here is an example: python from collections import namedtuple Coordinate = namedtuple "Coordinate", "x", "y" location = Coordinate 10, 20 match location: case Coordinate horizontal, vertical : print horizontal, vertical This would print 10 20 . Python decides which attributes go into horizontal and vertical by looking at match args on the class. print Coordinate. match args 'x', 'y' Names wrapped in double underscores are often called dunder names dunder comes from double underscore . They are special names used by Python’s object model. For the Coordinate class, match args contains: "x", "y" So the first position in the pattern reads x and the second reads y . This tuple can be changed: Coordinate. match args = "y", "x" With that change, the exact same pattern prints 20 10 . The tuple controls which attributes Python reads for the positional entries in the pattern. PEP 634 https://peps.python.org/pep-0634/ class-patterns specifies this class-pattern behaviour. CPython performs these attribute lookups internally, so they happen even if the getattr builtin exposed to user code has been removed. That matters for an AST validator. The validator on the sandbox only looks at the structure of the submitted code, and it was built to reject nodes that read sensitive attributes, such as obj. class an attribute access or getattr obj, " class " a call to a blocked builtin . A class pattern produces neither. In the AST, it is a single ast.MatchClass node containing the class name and the names to bind. The attribute name never appears in the code at the point where the read happens. It is stored in match args , and Python only looks it up while evaluating the pattern at runtime. Here is the same Coordinate example with match args pointed at a different attribute: Coordinate. match args = " class ", match location: case Coordinate c : print c This prints