Skip to content

fix: move OpenAI API key from client-side session cookie to server-side storage (CWE-200)#63

Open
sebastiondev wants to merge 1 commit intoBAI-LAB:mainfrom
sebastiondev:fix/cwe200-app-api-1b6f
Open

fix: move OpenAI API key from client-side session cookie to server-side storage (CWE-200)#63
sebastiondev wants to merge 1 commit intoBAI-LAB:mainfrom
sebastiondev:fix/cwe200-app-api-1b6f

Conversation

@sebastiondev
Copy link
Copy Markdown

Vulnerability Summary

CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
Severity: Medium
File: memoryos-playground/memdemo/app.py
Lines: L91–L102 (before fix)

Data Flow

HTTP POST /init_memory
  → api_key from request.json
  → stored in session["memory_config"]["api_key"]
  → Flask session cookie (base64-encoded, signed but NOT encrypted)
  → sent to browser, included in every subsequent HTTP request/response

Flask's default session implementation uses signed but not encrypted cookies. As Flask's own documentation states: "The session cookie is only signed, not encrypted. Users can read the contents."

This means the OpenAI API key is visible in plaintext (base64-decoded) to:

  • Anyone who can inspect network traffic (the app runs on HTTP without TLS)
  • Browser extensions with cookie access
  • Proxy/CDN/WAF logs that capture cookie headers
  • XSS attacks (the frontend has multiple innerHTML sinks with unsanitized LLM responses)
  • Anyone with physical access to the browser

The key is not only stored once — it is retransmitted with every request in the Cookie header and with every response in the Set-Cookie header, multiplying exposure windows.

Exploit Sketch

import base64, json

# Flask session cookie value (from network capture, browser, logs, etc.)
cookie = "<session cookie value>"

# Decode — Flask uses base64 (with optional zlib compression)
try:
    import zlib
    decoded = zlib.decompress(base64.b64decode(cookie + "=="))
except:
    decoded = base64.b64decode(cookie + "==")

data = json.loads(decoded)
print(f"Stolen API Key: {data['memory_config']['api_key']}")

Fix Description

Approach: Move the API key (and related config) from the client-side Flask session cookie to the existing server-side memory_systems dictionary.

What Changed

The memory_systems dict previously stored only the Memoryos instance per session. This fix restructures each entry to hold both the system and its config:

# Before (API key in client session cookie):
memory_systems[session_id] = memory_system
session["memory_config"] = {"api_key": api_key, "base_url": base_url, "model": model}

# After (API key stays server-side only):
memory_systems[session_id] = {
    "system": memory_system,
    "config": {"api_key": api_key, "base_url": base_url, "model": model},
}

All 7 access points where memory_systems[session_id] was used as a Memoryos instance are updated to use memory_systems[session_id]["system"]. The /clear_memory endpoint, which previously read the API key back from session["memory_config"], now reads it from the server-side entry["config"].

Rationale

  • The session cookie only needs to carry memory_session_id (an opaque token) — the API key has no reason to leave the server.
  • This eliminates persistent storage and repeated transmission of the secret in every HTTP request/response cycle.
  • The fix is minimal (1 file, +25/−18 lines) and preserves all existing functionality.

Test Results

Manual Verification

  1. All 7 access paths updated consistentlychat, get_memory_state, trigger_analysis, personality_analysis, clear_memory, import_conversations, and init_memory all correctly reference the new {'system': ..., 'config': ...} structure.
  2. /clear_memory reinitializaton now retrieves config from entry["config"] (server-side) instead of session.get("memory_config") (client cookie).
  3. No new dependencies introduced.
  4. No changes to the frontend or any other file.
  5. Session cookie now only contains memory_session_id — an opaque hex token with no sensitive data.

Diff Verification

 memoryos-playground/memdemo/app.py | 43 ++++++++++++++++++++++----------------
 1 file changed, 25 insertions(+), 18 deletions(-)

Disprove Analysis

We attempted to disprove and poke holes in our own finding. Summary of results:

Authentication Check

No authentication or authorization exists on any endpoint. The app is completely open to anyone who can reach port 5019.

Network Check

The app binds to 0.0.0.0:5019 with debug=True. No CORS restrictions, no HTTPS, no ALLOWED_HOSTS. It is internet-accessible when deployed.

Deployment Context

The README instructs users to run python3 app.py directly. A hosted version exists at baijia.online/memoryos/, confirming it is intended for internet-facing deployment. No reverse proxy, VPN, or TLS configuration is mentioned.

Caller Trace

session["memory_config"] is written in init_memory() at line 102. The API key flows from user POST body → session["memory_config"]["api_key"] → Flask session cookie (sent to browser as base64-encoded, signed but not encrypted data). It is read back in clear_memory() at line 355 to reinitialize the memory system.

Input Validation

No input sanitization, escaping, or validation of the API key before storing it in the session cookie.

Prior Reports / Security Policy

No prior security issues reported on this repository. No SECURITY.md exists.

Mitigations Found

None. No cookie flags (HttpOnly, Secure, SameSite), no TLS, no CSP headers, no auth.

Fix Adequacy Assessment

The fix is meaningful but scoped. It addresses the specific CWE-200 issue of storing secrets in client-readable cookies. It does not address parallel concerns:

  • The API key is still transmitted once in plaintext over HTTP in the initial /init_memory POST body (one-time event vs. persistent cookie retransmission).
  • debug=True remains, exposing the Werkzeug debugger.
  • No HttpOnly, Secure, or SameSite cookie flags.
  • Multiple innerHTML XSS sinks exist in the frontend.

These are noted as separate concerns for your awareness, not as reasons to reject this fix. Each represents a distinct vulnerability with a distinct remediation.

Verdict: CONFIRMED_VALID (Confidence: High)

The vulnerability is real, well-understood, and documented by Flask itself. The fix meaningfully reduces the attack surface by removing the API key from persistent, repeatedly-transmitted client-side storage and moving it to server-side-only storage.


Additional Notes for Maintainers

This PR intentionally limits scope to the CWE-200 session cookie issue. For defense-in-depth, you may also want to consider:

  1. Switching debug=True to debug=False for any deployment (Werkzeug debugger can enable RCE)
  2. Adding TLS (or documenting reverse proxy setup) to protect the initial API key POST
  3. Setting cookie flags (httponly=True, samesite="Lax") on the Flask session
  4. Sanitizing LLM output before inserting into innerHTML in the frontend
  5. Adding rate limiting and session expiration to prevent resource exhaustion

Thank you for your work on MemoryOS! Happy to discuss or adjust anything in this PR.

Flask default sessions are signed but not encrypted — the session
payload is base64-visible in the cookie.  Storing the OpenAI API key
in session["memory_config"] exposed it to anyone who can read the
cookie (network interception, XSS, browser extensions, etc.).

Move the config dict (containing api_key, base_url, model) into the
server-side memory_systems dict alongside the Memoryos instance.
Only the opaque session_id token is now stored in the cookie.

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant