Skip to content

auth: is_internal_request() authorises every request when INTERNAL_SERVICE_SECRET is unset (None == None) #2553

Open

Description

@hyouhaku

Summary

In src/memos/api/middleware/auth.py, is_internal_request() treats every request as an
internal service call when INTERNAL_SERVICE_SECRET is not configured, because both sides of the
comparison are None.

Code

def is_internal_request(request: Request) -> bool:
    """Check if request is from internal service."""
    client_host = request.client.host if request.client else None

    # Check internal IPs
    if client_host in INTERNAL_SERVICE_IPS:
        return True

    # Check internal header (for container-to-container)
    internal_header = request.headers.get("X-Internal-Service")
    return internal_header == os.getenv("INTERNAL_SERVICE_SECRET")

If INTERNAL_SERVICE_SECRET is unset, os.getenv(...) returns None. If the client sends no
X-Internal-Service header, request.headers.get(...) also returns None. So the function
returns None == None → True.

Impact

verify_api_key() consults this before validating the key:

if is_internal_request(request):
    return {"user_name": "internal", "scopes": ["all"], ...}

So with AUTH_ENABLED=true but INTERNAL_SERVICE_SECRET unset, any request — including one with
no credentials at all — is accepted as user_name="internal" with scopes: ["all"]. Enabling
AUTH_ENABLED without also setting INTERNAL_SERVICE_SECRET provides no protection, which is
easy to do since nothing warns about it.

Reproduce

  1. Set AUTH_ENABLED=true, leave INTERNAL_SERVICE_SECRET unset.
  2. Call any endpoint guarded by Depends(verify_api_key) (e.g. in routers/admin_router.py) from
    a non-loopback address, with no Authorization and no X-Internal-Service header.
  3. The request is authorised as internal with all scopes.

Suggested fix

Require the secret to be present and non-empty before comparing, so an unset secret fails closed:

    internal_secret = os.getenv("INTERNAL_SERVICE_SECRET")
    if not internal_secret:
        return False
    return secrets.compare_digest(
        request.headers.get("X-Internal-Service") or "", internal_secret
    )

Using secrets.compare_digest also avoids a timing side channel on the comparison.

It may also be worth logging a warning at startup when AUTH_ENABLED=true and
INTERNAL_SERVICE_SECRET is unset, since that combination currently looks secure but is not.

Note on INTERNAL_SERVICE_IPS

Separately, INTERNAL_SERVICE_IPS contains 127.0.0.1 / ::1, so any process that can reach the
server over loopback — a sidecar container, or a kubectl port-forward — is also authorised with
scopes: ["all"]. That may be intentional, but it is worth documenting, because loopback is not a
trust boundary in a shared pod or on a developer machine.

Related question

verify_api_key appears to be referenced only by routers/admin_router.py; server_api.py
includes server_router with no dependencies=[...], and its endpoints take user_id /
user_name / mem_cube_id as query or body parameters. Is the main memory API intended to be
unauthenticated and deployed on a private network only? If so, saying that explicitly in the
deployment docs would help — we had assumed cube isolation was enforced server-side per
authenticated caller, and it is not.

Version: a7367d07e55db61099f7b4e2c1108bc5831a24f3

Activity

  1. added
    ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务
    area:api云服务 / FastAPI / OpenAPI / MCP
    status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理
    types:bugSomething isn't working | 功能异常
    on Oct 31, 2026
  2. self-assigned this
    on Oct 31, 2026
  3. Memtensor-AI commented on Oct 31, 2026

    @Memtensor-AI
    Collaborator

    🤖 AutoDev has picked up this issue and started working on it.

    Task ID: 10c228629ce3a92a
    Working branch: bugfix/autodev-2553-20261031205634665
    Target branch: latest dev* branch
    Workflow: opsp (analysis → coding → testing → PR)

    I will post the PR link here once done. If I need more information, I will ask in the comments.

  4. added
    ai:testingAI agent is running tests | AI 正在运行测试
    and removed
    ai:codingAI agent is coding | AI 正在编码
    on Oct 31, 2026
  5. Memtensor-AI commented on Oct 31, 2026

    @Memtensor-AI
    Collaborator

    ✅ AutoDev task 10c228629ce3a92a completed.

    Summary: Fixes issue #2553. src/memos/api/middleware/auth.py::is_internal_request() previously compared the X-Internal-Service header to os.getenv("INTERNAL_SERVICE_SECRET") with plain ==. When operators enabled AUTH_ENABLED=true but forgot to configure the secret, both sides collapsed to None, so every anonymous external request was promoted to user_name="internal", scopes=["all"].

    The fix makes the header path fail closed: it now returns False whenever INTERNAL_SERVICE_SECRET is unset or empty, and uses secrets.compare_digest on the configured value to eliminate the timing side channel. A startup warning is logged at module import when AUTH_ENABLED=true is combined with an unset secret, so the misconfiguration surfaces instead of pretending to work. The INTERNAL_SERVICE_IPS loopback short-circuit is deliberately left untouched (the issue flags it as a separate discussion).

    Tests: a new regression suite tests/api/test_is_internal_request.py pins the contract — unset/empty secret always rejects (with and without a header value); configured secret authenticates only on exact match; loopback IPs still short-circuit; the end-to-end verify_api_key path returns 401 for anonymous external callers. Verification: pytest tests/api/test_is_internal_request.py tests/api/test_validate_key_format.py -q → 33 passed. ruff check src/memos/api/middleware/auth.py tests/api/test_is_internal_request.py → all checks passed. ruff format applied.

    No API surface, OpenAPI, or dependency changes. Branch pushed to origin; specs archive synced to memos-autodev-specs.

    Base branch: main
    Branch: bugfix/autodev-2553-20261031205634665
    Commit: b648341
    PR: #2554
    Assigned to: @hijzy
    Reviewers: @bittergreen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务ai:testingAI agent is running tests | AI 正在运行测试area:api云服务 / FastAPI / OpenAPI / MCPstatus:in-progressSomeone or AI is working on it | 人工或 AI 正在处理types:bugSomething isn't working | 功能异常

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions