Skip to content

Security Threat Model: Server Mode Exposes Critical Attack Surface #9941

@jmanico

Description

@jmanico

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)

  1. 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.

  2. 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.

  3. Validate outbound URLs in SNS, EventBridge, and Firehose — block metadata endpoints and private IP ranges when in server mode.

  4. Add a prominent security warning in the server mode documentation: "Do not expose moto server to untrusted networks."

  5. Make debug mode configurable — default to False in server mode, let users opt in.

  6. Add a USER directive to the Dockerfile — do not run as root.

  7. Enforce S3 object size limits — match the AWS 5TB limit, or at least something reasonable.

  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions