Skip to content

fix(auth): 用户名和用户ID校验拒绝末尾换行 - #1158

Merged
xerrors merged 1 commit into
xerrors:mainfrom
goranai:fix/username-trailing-newline
Nov 13, 2026
Merged

xerrors merged 1 commit into
xerrors:mainfrom
goranai:fix/username-trailing-newline

Conversation

@goranai

@goranai goranai commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

变更说明

validate_username and the two uid checks used re.match(r"^...$", value).
In Python $ also matches before a final \n, so "admin\n" passed.
get_by_username compares exactly, so "admin\n" passed the uniqueness check and was stored next to "admin".
It renders the same as "admin", for example in the share picker.

This PR uses re.fullmatch with the same character class at the three sites:

  • backend/yuxi/modules/identity/services/usernames.py validate_username
  • backend/yuxi/api/routers/identity/auth.py initialize_admin uid
  • backend/yuxi/modules/identity/services/administration.py create_department_with_admin admin_uid

It adds backend/test/unit/identity/test_usernames.py.

  • 任务类型:bug-fix
  • 目标与非目标:Goal: reject a trailing newline in usernames and uids. Non-goal: no change to allowed characters, lengths, existing stored rows, phone validation or the frontend.
  • substantial / trivial 判断:trivial. One-line regex change per site. It only rejects values that end with \n, which the error message and the frontend rule already say are not allowed.

Behavior change: the only change is that a username or uid ending in one \n is now rejected with the existing error message. Every other input gets the same result as before (checked by a 200000-string fuzz and a 5000-example hypothesis run, see below). Existing rows that already end in \n are not touched.

工程主张与 Owner

  • 受影响的工程主张:A username or uid accepted by the backend contains only 中文, letters, digits and _, with no hidden trailing newline.
  • Owner / commit point / 观察边界:validate_username in usernames.py owns the username rule. The inline checks in initialize_admin and create_department_with_admin own the uid rule. Observable as the 400 / InvalidOperationError response and as the stored users.username / users.uid.
  • 决策记录:None added. This restores the rule already stated by the error text and the frontend check, with no open alternative. I can add an implemented record if you want one.

验证情况

Trailing newline is rejected

  • 失败面:validate_username("admin\n") returned (True, "") and update_profile stored "admin\n" next to "admin".
  • 语义 Owner:validate_username, uid checks in auth.py and administration.py
  • 直接证据 / 命令:pytest test/unit/identity -q -> 28 passed (10 new). With usernames.py reverted, the new test test_validate_username_rejects_whitespace_and_symbols[admin\n] fails (1 failed, 9 passed).
  • 负向案例:\r, \r\n, inner or leading newline, \n\n, U+2028, U+0085, VT, FF, NUL, fullwidth letters, spaces, -, ., empty, length 1 and 21: rejected before and after.
  • 结果:Passed

No valid name is newly rejected

  • 失败面:a stricter regex could reject valid names.
  • 语义 Owner:validate_username, uid checks
  • 直接证据 / 命令:local script comparing old and new against a per-character oracle. 200000 random strings: new code 0 wrong accepts, 0 wrong rejects (old code 268 wrong accepts for usernames, 48 for uids). Hypothesis st.text, 5000 examples: old and new differ only on a trailing \n.
  • 负向案例:ad, 20 chars, Admin, ADMIN, 张三, 龥龥, a_1, __, 01 still accepted.
  • 结果:Passed

Lint and repo guards

  • 失败面:style or engineering contract checks
  • 语义 Owner:不适用
  • 直接证据 / 命令:ruff 0.16.4 (from uv.lock) in backend/: ruff check yuxi passed, ruff format yuxi --check 448 files already formatted, ruff check --select I yuxi passed, same on the new test file. python scripts/ci/verify_engineering_contracts.py passed.
  • 负向案例:不适用
  • 结果:Passed

Integration and full unit suite

  • 失败面:identity flows against Postgres
  • 语义 Owner:identity services
  • 直接证据 / 命令:test/integration identity tests, full pytest test/unit, make test
  • 负向案例:不适用
  • 结果:Not run. I ran on Windows without Docker. Only test/unit/identity was run. CI will run the full unit suite.

简化 / 删除验收

不涉及

独立语义 Review

A fresh-context reviewer read the full diff (three one-line regex changes and the new test file) and the traced request-to-storage path. It checked every number and command in this body against the validation run. Conclusion: the change is correct and small, and the impact is low (display-name lookalike, no login or access change). No unresolved items. This review does not replace the test evidence above.

未验证范围与风险

  • Integration tests and the full unit suite were not run locally (need Docker and Linux).
  • End-to-end HTTP on Postgres was not run. The storage repro used SQLite with the real User model and update_profile.
  • Existing rows that already end with \n stay as they are. No migration.

事故反馈

Not a high-impact escape. Display-name lookalike only, no login or access change. No postmortem.

界面变更

不涉及

关联事项

Closes #1157

补充说明

The same ^...$ pattern is in modules/knowledge/evaluation/service.py for run_id. It is followed by an exact DB lookup, so it is harmless and left out of this PR.

Validated with Claude Opus 5.5.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xerrors
xerrors merged commit 11b9813 into xerrors:main Nov 13, 2026
20 checks passed
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.

Error: 用户名校验接受末尾换行,可创建与他人显示名完全相同的用户名 (validate_username accepts a trailing newline)

2 participants