Skip to content

fix(api): enforce canonical hex format on validate_key_format (#2427) - #2491

Open
RerankerGuo wants to merge 1 commit into
MemTensor:dev-v2.0.36from
RerankerGuo:fix/issue-2427-canonical-hex
Open

RerankerGuo wants to merge 1 commit into
MemTensor:dev-v2.0.36from
RerankerGuo:fix/issue-2427-canonical-hex

Conversation

@RerankerGuo

Copy link
Copy Markdown
Contributor

Summary

validate_key_format exists as a separate copy in two modules. Both used int(hex_part, 16) to verify the 64-char hex tail, which silently passes for leading signs, 0x prefixes, _ separators, and surrounding whitespace — so the documented format krlk_<64-lowercase-hex> was not actually enforced at the middleware boundary.

Root cause

Two copies of the same buggy validator:

  • src/memos/api/utils/api_keys.py:48 — used by the key-creation utilities
  • src/memos/api/middleware/auth.py:65 — gates every authenticated request

Both check the tail via int(hex_part, 16). CPython accepts:

input int(..., 16) result
+aaaa… (64 chars) valid
0xaaaa… (62 chars) valid
aaaa_bbbb… (with _) valid (PEP 515)
AAAA… / AAAA… valid

So krlk_+a..a, krlk_0x.., krlk_a_a, krlk_A..A, etc. were all accepted by the middleware, hashed verbatim, and looked up against the database — bypassing the canonical form that generate_api_key() produces (lowercase, no prefixes, no separators, no signs, no whitespace).

Fix

Two changes that together eliminate the bug class:

  1. Canonical validator — src/memos/api/utils/api_keys.py: replace the int(hex_part, 16) check with an anchored regex re.fullmatch(r"[0-9a-f]{64}", hex_part). Only the exact 64-lowercase-hex form is valid.
  2. Dedup — src/memos/api/middleware/auth.py: import the canonical implementation under the name validate_key_format_canonical, keep the public symbol validate_key_format as a one-line re-export. This prevents the two copies from drifting again (the original bug class).

Tests

New tests/api/test_validate_key_format.py, 21 cases:

  • canonical format and generate_api_key() output (20 random samples) always pass
  • 15 non-canonical variants are rejected: signs, 0x prefix, underscore separators, uppercase hex, leading/trailing whitespace, wrong length, missing krlk_ prefix, Bearer prefix, empty string
  • both utils and middleware copies enforce the same contract

Verification

PYTHONPATH=src pytest tests/api/test_validate_key_format.py -v
============================== 21 passed in 3.31s ==============================

Ruff: ruff format --check and ruff check both clean on touched files (0 new findings vs main; the 3 pre-existing DTZ/datetime.utcnow warnings in api_keys.py are out of scope).

Out of scope

  • No new dependencies
  • No public API / OpenAPI contract changes
  • No DB schema, migration, or graph model changes
  • No Makefile / pyproject.toml / pre-commit / GitHub workflow changes

Closes #2427

…sor#2427)

validate_key_format exists as a separate copy in two modules — src/memos/api/utils/api_keys.py (canonical home, used by the key-creation utilities) and src/memos/api/middleware/auth.py (the middleware that gates every authenticated request). Both used `int(hex_part, 16)` to verify the 64-char hex tail, which silently passes for:

- a leading `+` or `-` sign (`int("+a..a", 16)` == ...),
- a `0x` / `0X` prefix (`int("0xa..", 16)` == ...),
- `_` separators (PEP 515: `int("a_a", 16)`),
- surrounding whitespace.

So the documented format `krlk_<64-lowercase-hex>` was not actually enforced — keys like `krlk_+a..a`, `krlk_0x..`, or `krlk_a_a` were accepted by the middleware, hashed verbatim, and looked up against the database, bypassing the canonical form that `generate_api_key()` produces.

This commit:

- Replaces the `int(hex_part, 16)` check in the canonical api_keys.py
  implementation with an anchored regex (`[0-9a-f]{64}`) so only the exact
  lowercase 64-hex form is valid.
- Imports the fixed implementation into auth.py and keeps the public
  `validate_key_format` name as a thin re-export, so the middleware
  shares the same logic as the canonical utils copy. This prevents the
  two copies from drifting again — the original bug class.

New tests/api/test_validate_key_format.py covers 21 cases:

- canonical format and `generate_api_key()` output always pass
- 15 non-canonical variants are rejected (signs, 0x prefix, underscore,
  uppercase, whitespace, wrong length, missing prefix, empty)
- both copies (utils and middleware) enforce the same contract

Fixes MemTensor#2427.
@Memtensor-AI Memtensor-AI added area:api 云服务 / FastAPI / OpenAPI / MCP status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Oct 13, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2491
Task: 7a559602b7cb116e
Base: dev-v2.0.36
Head: fix/issue-2427-canonical-hex

🔍 OpenCodeReview found 1 issue(s) in this PR.


1. tests/api/test_validate_key_format.py (L70-L71)

This case is labeled "Non-string" but the value is an empty string, so the list contains no actual non-string input (None, bytes, int). The empty string is still useful (it exercises the not key early return rather than the startswith branch), but the label is wrong and the isinstance(key, str) guard in the canonical implementation is left uncovered — a validator that dropped that guard and called startswith/fullmatch directly would make this suite raise AttributeError on a real non-string, which these cases would not catch. Relabel the "" entry and add genuine non-string inputs.

💡 Suggested Change

Before:

            # Non-string
            "",

After:

            # Empty string (hits the `not key` early return)
            "",
            # Non-string
            None,
            b"krlk_" + b"a" * 64,

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (21/21 executed). memos_python_core/changed-repo-python: 21/21. Duration: 9s

Branch: fix/issue-2427-canonical-hex

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:api 云服务 / FastAPI / OpenAPI / MCP status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants