Overview
I spent some time going through the moto codebase with a security lens — specifically looking at what happens when moto runs in server mode or proxy mode rather than just as an in-process test decorator. The findings are significant enough that I think they deserve a consolidated write-up and discussion.
The core issue: moto was built as a testing library, but server mode turns it into a network-accessible service. The security posture appropriate for an in-process mock becomes dangerous when that mock is reachable over a network.
Methodology
I walked through this using a combination of STRIDE analysis, attack tree construction, and direct code review — focusing on trust boundaries, entry points, data flows, and existing controls.
Critical Findings
1. Remote Code Execution via Unsafe YAML Deserialization
Severity: Critical
The CloudFormation template parser uses yaml.Loader instead of yaml.SafeLoader:
moto/cloudformation/models.py (lines ~495, ~715)
moto/cloudformation/responses.py (lines ~71, ~485)
yaml.add_multi_constructor("", yaml_tag_constructor)
return yaml.load(template, Loader=yaml.Loader)
yaml.Loader allows arbitrary Python object instantiation. A crafted CloudFormation template with something like !!python/object/apply:os.system ["whoami"] would execute arbitrary commands on the moto server. This is the single most dangerous finding — it is a straightforward RCE in any server-mode deployment.
Complicating factor: CloudFormation legitimately uses custom YAML tags (!Ref, !Sub, !GetAtt, etc.), which is why a multi-constructor is registered. But yaml.SafeLoader with explicit constructors for just the CloudFormation tags would be the correct approach.
Recommendation: Replace yaml.Loader with yaml.SafeLoader and register only the specific CloudFormation tag constructors needed. This is the highest-priority fix.
2. No Authentication in Server Mode (By Design, But Dangerous)
Severity: Critical in server-mode deployments
# moto/settings.py
INITIAL_NO_AUTH_ACTION_COUNT = float(
os.environ.get("INITIAL_NO_AUTH_ACTION_COUNT", float("inf"))
)
Authentication is disabled by default (the counter is set to infinity). This is fine for in-process mocking but means any network client can make any AWS API call against a server-mode deployment with zero credentials.
Even when auth is enabled, the /moto-api/reset-auth endpoint lets anyone disable it:
# moto/moto_api/_internal/responses.py
def reset_auth_response(self, request, full_url, headers):
if request.method == "POST":
settings.INITIAL_NO_AUTH_ACTION_COUNT = float(request.data.decode())
ActionAuthenticatorMixin.request_count = 0
POST inf to /moto-api/reset-auth and auth is gone. This endpoint itself has no authentication.
3. Multiple Server-Side Request Forgery (SSRF) Vectors
Severity: High
Several AWS service implementations make outbound HTTP requests to user-specified URLs with no validation:
| Service |
File |
Mechanism |
| SNS |
moto/sns/models.py |
HTTP/HTTPS subscription endpoints — requests.post(self.endpoint, ...) |
| EventBridge |
moto/events/models.py |
API destination rules — requests.request(method, url, ...) |
| Firehose |
moto/firehose/models.py |
HTTP delivery endpoints — requests.post(url, ...) |
| Recorder |
moto/moto_api/_internal/recorder/models.py |
Replay recording — reads entries from file and replays HTTP requests |
In server mode, an attacker can create an SNS subscription pointing to http://169.254.169.254/latest/meta-data/ (or any internal service), publish a message, and exfiltrate the response. Same pattern applies to EventBridge and Firehose.
The recorder endpoint is particularly interesting: upload a crafted recording via /moto-api/recorder/upload-recording, then trigger /moto-api/recorder/replay-recording to fire arbitrary HTTP requests from the server.
Recommendation: When running in server mode, validate outbound URLs and block RFC1918 ranges + link-local addresses (169.254.x.x). At minimum, document the SSRF risk prominently.
4. Unauthenticated Admin API
Severity: High
The /moto-api/ endpoints provide full administrative control with zero authentication:
| Endpoint |
What It Does |
POST /moto-api/reset |
Wipes all mock state |
POST /moto-api/reset-auth |
Disables authentication |
POST /moto-api/seed |
Sets the random seed |
POST /moto-api/config |
Modifies runtime configuration |
POST /moto-api/proxy/passthrough |
Controls proxy bypass rules |
POST /moto-api/recorder/upload-recording |
Writes arbitrary data to disk |
POST /moto-api/recorder/replay-recording |
Triggers SSRF via recorded requests |
GET /moto-api/data.json |
Dumps all model data |
Any of these can be called by anyone who can reach the moto server. The data.json endpoint is essentially a full state exfiltration endpoint. The upload-recording + replay-recording chain is an SSRF primitive.
High/Medium Findings
5. Lambda Container to Admin API Lateral Movement
Severity: High
When Lambda functions execute in Docker containers, the container receives MOTO_HOST and MOTO_HTTP_ENDPOINT environment variables pointing back to the moto server. Since the admin API is unauthenticated, any code running inside a Lambda container can:
- Reset all moto state via
/moto-api/reset
- Disable authentication via
/moto-api/reset-auth
- Exfiltrate all data via
/moto-api/data.json
- Trigger SSRF via the recorder endpoints
This creates a lateral movement path from Lambda code execution to full moto control.
6. Flask Debug Mode Hardcoded
Severity: Medium
# moto/moto_server/werkzeug_app.py, line 329
backend_app.debug = True
# moto/server.py, line 71
main_app.debug = True
Debug mode is hardcoded to True. This exposes detailed stack traces including internal file paths, variable values, and code structure. Depending on the Werkzeug version, this could enable the interactive debugger (which is itself an RCE vector).
7. Docker Container Runs as Root
Severity: Medium
The Dockerfile has no USER directive, so the container runs as root. Combined with binding to 0.0.0.0 and the other findings, this increases blast radius if the container is compromised.
8. Permissive CORS Configuration
Severity: Medium (Server Mode)
# moto/moto_server/werkzeug_app.py
if not DISABLE_GLOBAL_CORS:
CORS(backend_app) # Default: allows all origins
Flask-CORS with default settings allows requests from any origin. A malicious webpage could make cross-origin requests to a locally running moto server. Disableable via MOTO_DISABLE_GLOBAL_CORS=true, but the default is wide open.
9. No Upload Size Limits for S3
Severity: Medium
S3 object uploads have no enforced size limits at the model level. AWS limits objects to 5TB, but moto does not enforce this. The multipart completion handler concatenates all parts into an in-memory bytearray, so a sufficiently large upload can exhaust memory. SpooledTemporaryFile will also spill to disk, potentially filling the filesystem.
10. Incomplete IAM Policy Evaluation
Severity: Medium (Testing Gap)
The IAM access control implementation explicitly does not support:
- Resource-level conditions
Condition element evaluation
- Resource-based policies (beyond basic S3/EC2)
- Many service-specific authorization behaviors
If teams use moto to validate their IAM policies, they may have a false sense of security — policies that appear to work in moto may have gaps that only surface against real AWS.
What is Actually Fine
A few things I checked that turned out to be non-issues:
- XML parsing: Uses
xml.etree.ElementTree and xmltodict, both based on expat — not vulnerable to XXE in Python 3
- Jinja2 templates: All template sources are hardcoded in the codebase, not user-controlled — no SSTI risk
- No pickle in production code: Only used in a test to verify S3 FakeKey objects are pickleable
- No eval/exec: Clean on this front
- S3 key handling: Key names are stored as metadata strings, not used to construct filesystem paths — no path traversal
Attack Trees: Highest-Risk Paths
Path 1: RCE via CloudFormation (Lowest Barrier)
Root: Execute arbitrary code on moto server
Submit crafted CloudFormation template (OR)
CreateStack API with YAML payload containing !!python/object/apply tag
UpdateStack API with malicious template
Prerequisites: Network access to moto server (no auth required by default)
Cost: Low | Difficulty: Low | Detectability: Low
Path 2: SSRF Data Exfiltration
Root: Access internal network resources via moto
Create SNS HTTP subscription to internal URL (OR)
Subscribe to http://169.254.169.254/latest/meta-data/
Subscribe to internal service endpoint
Use recorder upload+replay for arbitrary HTTP requests
Then: Publish message to trigger outbound request
Prerequisites: Network access (no auth)
Cost: Low | Difficulty: Low | Detectability: Medium
Path 3: Full State Compromise
Root: Exfiltrate all mock data + disable controls
GET /moto-api/data.json (exfiltrate everything)
POST /moto-api/reset-auth with body "inf" (disable auth)
POST /moto-api/reset (destroy evidence)
Prerequisites: Network access to moto server
Cost: Low | Difficulty: Low | Detectability: Low
Recommendations (Priority Order)
-
Fix the YAML deserialization — switch to yaml.SafeLoader with explicit constructors for CloudFormation tags. This is the most dangerous finding and the easiest to fix.
-
Add authentication to /moto-api/* endpoints — or at minimum, make them opt-in in server mode. Consider a shared secret or binding them to localhost only.
-
Validate outbound URLs in SNS, EventBridge, and Firehose — block metadata endpoints and private IP ranges when in server mode.
-
Add a prominent security warning in the server mode documentation: "Do not expose moto server to untrusted networks."
-
Make debug mode configurable — default to False in server mode, let users opt in.
-
Add a USER directive to the Dockerfile — do not run as root.
-
Enforce S3 object size limits — match the AWS 5TB limit, or at least something reasonable.
-
Document the IAM policy evaluation gaps so teams do not rely on moto for authorization testing without understanding the limitations.
Scope Note
This analysis focused on the security posture of moto when running in server mode or proxy mode. Most of these findings are non-issues when moto is used purely as an in-process test decorator (its primary use case). However, the server mode documentation and Docker image suggest it is intended to be deployed as a service, and in that context these findings are real risks.
I recognize this is a testing tool, not a production service. But people do run moto server in shared environments (CI/CD, development clusters, shared test infrastructure), and in those contexts, these attack surfaces matter.
Overview
I spent some time going through the moto codebase with a security lens — specifically looking at what happens when moto runs in server mode or proxy mode rather than just as an in-process test decorator. The findings are significant enough that I think they deserve a consolidated write-up and discussion.
The core issue: moto was built as a testing library, but server mode turns it into a network-accessible service. The security posture appropriate for an in-process mock becomes dangerous when that mock is reachable over a network.
Methodology
I walked through this using a combination of STRIDE analysis, attack tree construction, and direct code review — focusing on trust boundaries, entry points, data flows, and existing controls.
Critical Findings
1. Remote Code Execution via Unsafe YAML Deserialization
Severity: Critical
The CloudFormation template parser uses
yaml.Loaderinstead ofyaml.SafeLoader:moto/cloudformation/models.py(lines ~495, ~715)moto/cloudformation/responses.py(lines ~71, ~485)yaml.Loaderallows arbitrary Python object instantiation. A crafted CloudFormation template with something like!!python/object/apply:os.system ["whoami"]would execute arbitrary commands on the moto server. This is the single most dangerous finding — it is a straightforward RCE in any server-mode deployment.Complicating factor: CloudFormation legitimately uses custom YAML tags (
!Ref,!Sub,!GetAtt, etc.), which is why a multi-constructor is registered. Butyaml.SafeLoaderwith explicit constructors for just the CloudFormation tags would be the correct approach.Recommendation: Replace
yaml.Loaderwithyaml.SafeLoaderand register only the specific CloudFormation tag constructors needed. This is the highest-priority fix.2. No Authentication in Server Mode (By Design, But Dangerous)
Severity: Critical in server-mode deployments
Authentication is disabled by default (the counter is set to infinity). This is fine for in-process mocking but means any network client can make any AWS API call against a server-mode deployment with zero credentials.
Even when auth is enabled, the
/moto-api/reset-authendpoint lets anyone disable it:POST
infto/moto-api/reset-authand auth is gone. This endpoint itself has no authentication.3. Multiple Server-Side Request Forgery (SSRF) Vectors
Severity: High
Several AWS service implementations make outbound HTTP requests to user-specified URLs with no validation:
moto/sns/models.pyrequests.post(self.endpoint, ...)moto/events/models.pyrequests.request(method, url, ...)moto/firehose/models.pyrequests.post(url, ...)moto/moto_api/_internal/recorder/models.pyIn server mode, an attacker can create an SNS subscription pointing to
http://169.254.169.254/latest/meta-data/(or any internal service), publish a message, and exfiltrate the response. Same pattern applies to EventBridge and Firehose.The recorder endpoint is particularly interesting: upload a crafted recording via
/moto-api/recorder/upload-recording, then trigger/moto-api/recorder/replay-recordingto fire arbitrary HTTP requests from the server.Recommendation: When running in server mode, validate outbound URLs and block RFC1918 ranges + link-local addresses (169.254.x.x). At minimum, document the SSRF risk prominently.
4. Unauthenticated Admin API
Severity: High
The
/moto-api/endpoints provide full administrative control with zero authentication:POST /moto-api/resetPOST /moto-api/reset-authPOST /moto-api/seedPOST /moto-api/configPOST /moto-api/proxy/passthroughPOST /moto-api/recorder/upload-recordingPOST /moto-api/recorder/replay-recordingGET /moto-api/data.jsonAny of these can be called by anyone who can reach the moto server. The
data.jsonendpoint is essentially a full state exfiltration endpoint. Theupload-recording+replay-recordingchain is an SSRF primitive.High/Medium Findings
5. Lambda Container to Admin API Lateral Movement
Severity: High
When Lambda functions execute in Docker containers, the container receives
MOTO_HOSTandMOTO_HTTP_ENDPOINTenvironment variables pointing back to the moto server. Since the admin API is unauthenticated, any code running inside a Lambda container can:/moto-api/reset/moto-api/reset-auth/moto-api/data.jsonThis creates a lateral movement path from Lambda code execution to full moto control.
6. Flask Debug Mode Hardcoded
Severity: Medium
Debug mode is hardcoded to
True. This exposes detailed stack traces including internal file paths, variable values, and code structure. Depending on the Werkzeug version, this could enable the interactive debugger (which is itself an RCE vector).7. Docker Container Runs as Root
Severity: Medium
The Dockerfile has no
USERdirective, so the container runs as root. Combined with binding to0.0.0.0and the other findings, this increases blast radius if the container is compromised.8. Permissive CORS Configuration
Severity: Medium (Server Mode)
Flask-CORS with default settings allows requests from any origin. A malicious webpage could make cross-origin requests to a locally running moto server. Disableable via
MOTO_DISABLE_GLOBAL_CORS=true, but the default is wide open.9. No Upload Size Limits for S3
Severity: Medium
S3 object uploads have no enforced size limits at the model level. AWS limits objects to 5TB, but moto does not enforce this. The multipart completion handler concatenates all parts into an in-memory
bytearray, so a sufficiently large upload can exhaust memory.SpooledTemporaryFilewill also spill to disk, potentially filling the filesystem.10. Incomplete IAM Policy Evaluation
Severity: Medium (Testing Gap)
The IAM access control implementation explicitly does not support:
Conditionelement evaluationIf teams use moto to validate their IAM policies, they may have a false sense of security — policies that appear to work in moto may have gaps that only surface against real AWS.
What is Actually Fine
A few things I checked that turned out to be non-issues:
xml.etree.ElementTreeandxmltodict, both based onexpat— not vulnerable to XXE in Python 3Attack Trees: Highest-Risk Paths
Path 1: RCE via CloudFormation (Lowest Barrier)
Path 2: SSRF Data Exfiltration
Path 3: Full State Compromise
Recommendations (Priority Order)
Fix the YAML deserialization — switch to
yaml.SafeLoaderwith explicit constructors for CloudFormation tags. This is the most dangerous finding and the easiest to fix.Add authentication to
/moto-api/*endpoints — or at minimum, make them opt-in in server mode. Consider a shared secret or binding them to localhost only.Validate outbound URLs in SNS, EventBridge, and Firehose — block metadata endpoints and private IP ranges when in server mode.
Add a prominent security warning in the server mode documentation: "Do not expose moto server to untrusted networks."
Make debug mode configurable — default to
Falsein server mode, let users opt in.Add a
USERdirective to the Dockerfile — do not run as root.Enforce S3 object size limits — match the AWS 5TB limit, or at least something reasonable.
Document the IAM policy evaluation gaps so teams do not rely on moto for authorization testing without understanding the limitations.
Scope Note
This analysis focused on the security posture of moto when running in server mode or proxy mode. Most of these findings are non-issues when moto is used purely as an in-process test decorator (its primary use case). However, the server mode documentation and Docker image suggest it is intended to be deployed as a service, and in that context these findings are real risks.
I recognize this is a testing tool, not a production service. But people do run moto server in shared environments (CI/CD, development clusters, shared test infrastructure), and in those contexts, these attack surfaces matter.