Breaking the AWS MCP Python Sandbox
Preface AWS announced the general availability of the AWS MCP Server on May 6, 2026. It is a managed remote Model Context Protocol (MCP) server.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
Preface AWS announced the general availability of the AWS MCP Server 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 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, 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 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: 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 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: 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 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
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。