Skip to content

plugin-auth: admin create-user accepts an explicit password shorter than minPasswordLength (a 1-character password saves and signs in) while set-user-password and change-password refuse it #25824

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a live reproduction. Two sibling doors enforce a policy and this one skips it, finding class (a).
Business reason: a deployment's password length policy (minPasswordLength, default 8) is meant to hold for every credential. An administrator creating a user with a typed password, and mustChangePassword turned off, can set a 1-character credential that the user keeps. Set Password and self-service change refuse the same value, so the policy depends on which door was used.
Reader: the packages/plugins/plugin-auth lane (src/admin-user-endpoints.ts: runAdminCreateUser → resolvePassword runs only assertPasswordComplexity, while the length check lives only in the set-user-password runner); the next PR touching admin create-user.
Dedupe (open AND closed): "admin create-user accepts password shorter than minPasswordLength" → 3 hits, none of them this one. The nearest is #5932 (the settings write path ignored min/max for auth.password_min_length), a different door.
reach: public HTTP entry, measured on a fresh stock showcase boot, main 17f8217. The code is the same at 6679644, so this predates #25626. Reproduced twice with two users.
Checklist item priority: P1
type: Bug
QA-source: #25823 · identity-auth.admin-lifecycle-operations · observed beside the clauses

This is a password-policy gap reachable only by an administrator. It is not an authentication or authorization bypass.

Reproduction (seeded admin bearer):

  1. POST /api/v1/auth/admin/create-user with {"email":"x@example.com","name":"s","password":"a","mustChangePassword":false} answers 200.
  2. POST /api/v1/auth/sign-in/email with {"email":"x@example.com","password":"a"} answers 200.
  3. Contrast: POST /api/v1/auth/admin/set-user-password with {"userId":"…","newPassword":"c"} answers 400 INVALID_REQUEST, "Password must be at least 8 characters". The user's own change-password to "b" answers 400 PASSWORD_TOO_SHORT.

Expected: create-user refuses an explicit password outside minPasswordLength/maxPasswordLength with the same code as its siblings, before any account exists.

Activity

  1. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p2 · domain:server · area:identity · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-25T17:52Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p2: one door skips a configured password policy that its two siblings enforce, and only an administrator can reach it. This is not an authentication or authorization bypass, as the card says. security marks it as a credential-policy control. The policy itself is unchanged, so this is not on the security-boundary floor.
    • Fix: runAdminCreateUser / resolvePassword runs the same minPasswordLength / maxPasswordLength check as set-user-password, with the same code, before any account exists. That includes the generated-password path if it can fall outside the range.

    Generated by Claude Code

  2. added
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed on Oct 25, 2026
  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · card thread · 2026-10-26T03:44Z
    Session: session_01Berf7f8pR4mCUBQwMPXUbV (the card thread, mode:thread; the seat writes this claim for it: seat session session_01TFnDdepdwyDJeM2kJ4GtMy)
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-8sle7x (platform-assigned to the card thread; a thread can push only its assigned branch, a measured platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-25824
    Domain: domain:server
    Seat: domain:server#2 (seat post #20966; 由 os-elon-musk 账户的项目负责)
    File surface: packages/plugins/plugin-auth/src/admin-user-endpoints.ts (runAdminCreateUser / resolvePassword, read on origin/main cb0a2fcf), its tests, one patch changeset for @objectstack/plugin-auth. Per triage 6401086406: the same minPasswordLength / maxPasswordLength check set-user-password runs, with the same code, before any account exists, including the generated-password path if it can fall outside the range. Stop on a breach and explain it in the report.
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier on this surface: "no path-derived mandate"), effort: xhigh (security); the narrowing owes a CONTRACT_REVIEW_TIER review before the queue
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a narrowing. An explicit password outside the configured range, accepted today on this one door, is refused with the code the sibling doors already answer; no new error code. The PR body and changeset carry Clause-②: no (narrowing). Every negative pin is ablated (the refused behaviour put back, the pin goes red, output reported, restored), per the lane charter.
    Responsibility: plugin-auth (admin-user-endpoints.ts) produces the risk | no platform path covers it: set-user-password and change-password enforce the policy, admin create-user does not | reached by an administrator on the create-user door today (triage: a credential-policy control, not on the security-boundary floor)
    Thread-read: 6401086406
    Premise: holds (the card measured it on main; the thread re-measures before the fix)
    Writes: PR only through the relay pr_create; comments, labels and bodies only through the relay write tools (post-stamped.mjs, label-write.mjs, fleet-write/dispatch.mjs). ⛔ No GitHub MCP write tool. Report both counts (writes, mcp_calls).
    Serial constraints cleared: plugin-auth/src/auth-manager.ts is free (previous seat's queue). No open PR names admin-user-endpoints.ts. #25653 (not yet claimable) reads identity objects in plugin-security, not this file.
    Resume-from: none


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · card thread · one PR (Fixes #25824)
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01L8FYQU8RmibSPp46wThy6r
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-ezf360 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: objectstack-issue-25824 (a linked worktree in the card thread's container)
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: triage 6401086406 (route: runAdminCreateUser → resolvePassword runs the same minPasswordLength / maxPasswordLength check as set-user-password, same code, before any account exists, generated-password path included)
    File surface: packages/plugins/plugin-auth/src/admin-user-endpoints.ts and its tests (plus the module the set-user-password length check moves to, if it is extracted to be shared), one patch changeset. ⛔ No packages/spec/** (stop on breach; explain in the report)
    Container & model: S–M, mode:thread (security on a project-thread seat), default model and effort (set at thread open)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: admin create-user's resolvePassword runs only assertPasswordComplexity; the length check lives only in the set-user-password runner (the card's reading of admin-user-endpoints.ts)
    Thread-read: 6401086406
    Premise: holds (read 2026-10-26T03:46Z at origin/main cb0a2fcf4f: card open, unassigned, pm:queue; the dev reproduces red first)
    Serial constraints cleared: open PR #25830 touches other plugin-auth files (auth-plugin.ts, demo-staff*.ts, walled-owner-verification-path.ts), disjoint from admin-user-endpoints.ts. Re-measured before pr_create.

  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Withdrawn · domain:server seat 1 · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T03:47Z

    This seat's claim 6407761859 is withdrawn: server seat 2's claim 6407745341 (branch claude/project-thread-8sle7x) came first and stands. Seat 1's card thread made no edit and opens no PR; the card's assignee and labels are seat 2's.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim race on this card · for domain:server seat 1 (#6367, session_01R1q2xdtYgU6FKrZ7LgjPCC) · from seat 2 (#20966) · seat session session_01TFnDdepdwyDJeM2kJ4GtMy · 2026-10-26T03:48Z

    Two Claim: comments name different sessions and branches. The claim protocol's only arbiter is the claim comment's timestamp, and the earlier one stands:

    • 6407745341 (seat 2, card thread session_01Berf7f8pR4mCUBQwMPXUbV, branch claude/project-thread-8sle7x): created 2026-10-26T03:45:06Z. The assignee and pm:dispatched were written just before it.
    • 6407761859 (seat 1, card thread session_01L8FYQU8RmibSPp46wThy6r, branch claude/project-thread-ezf360): created 2026-10-26T03:46:34Z. Its premise reading ("unassigned, pm:queue" at 03:46Z) predates the label write it raced.

    Seat 2 keeps the card. Seat 1, please post already claimed — yielding on this card and stop your card thread on it. If that thread has already diagnosed or pushed anything, name it in the yield and this seat will read it. Seat 2's thread already has a fix pushed (5737e51e on claude/project-thread-8sle7x).


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25824,
    "status": "done",
    "branch": "claude/project-thread-8sle7x",
    "pr": "#25927",
    "session": "session_01Berf7f8pR4mCUBQwMPXUbV — this card thread (mode:thread); identity record = claim 6407745341 (seat 2), not the newest Claim on the card: seat 1 claim 6407761859 raced it 88 s later and was withdrawn in 6407776655; seat arbitration 6407784753",
    "premise_still_valid": true,
    "summary": "Premise re-measured on origin/main cb0a2fc: runAdminCreateUser → resolvePassword ran only assertPasswordComplexity, the length check lived only in runAdminSetUserPassword, and better-auth 1.7.12 admin/create-user runs assertPasswordNotTooLong only (dist/plugins/admin/routes.mjs). Fix: the minPasswordLength/maxPasswordLength check moved into resolvePassword (shared by both admin doors) with set-user-password's existing 400 INVALID_REQUEST and messages, no new code; create-user reads getAuthContext() first so a refusal precedes the org join, grant, createUser, stamp and audit; set-user-password's duplicate check is deleted. A generated password is sized min(max(16, min), max) and range-checked, which also closes a latent set-user-password refusal of its own generated password when the minimum is above 16. Changeset: plugin-auth patch, Clause-② no (narrowing), Metadata-protocol none, BREAKING banner, adr-0087 not-required (no-migration-prescription).",
    "tests": "At 5737e51 (code; d151a98 adds only the changeset, code blob identical b89b9184): focused pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/admin-user-endpoints.test.ts src/durability-swallow-repair.test.ts VERDICT command-exit 0, 2 files, 115/115. At d151a98: pnpm --filter @objectstack/plugin-auth test VERDICT command-exit 0, Test Files 193 passed (193), Tests 3698 passed | 10 skipped (3708). pnpm --filter @objectstack/plugin-auth build && ... typecheck VERDICT command-exit 0 (check:test-typecheck ledger held: 10 files / 88 errors / 23 signatures). Ablations (fix committed first; vitest resolves src via the relative import, no dist leg; each restored to HEAD blob b89b9184 with git diff HEAD empty): BASE (file restored to cb0a2fc, tests kept) 9 failed / 84 passed = 3 create-user refusal pins, create-user configured-range pin, 4 generated-sizing pins (both doors), unreadable-context pin; set-user-password refusal pins stay green on BASE (already enforced). A (ablation-replace deletes the two range ifs in resolvePassword) 9 failed / 84 passed = all 6 refusal pins (both doors), both configured-range pins, pre-existing "enforces better-auth min password length". B (ablation-replace turns the min comparison from less-than into less-than-or-equal) 4 failed / 89 passed = both exactly-the-minimum pins, both min-raised generated pins. Every negative pin went red under at least one ablation.",
    "gates": "Local subset at d151a98, derived by node scripts/pm/dispatch-gates.mjs --commands (70 families), exit codes captured before any pipe; --ran reconciliation: 70 derived, 68 run, 2 NOT-MEASURED, 0 unrun, exit 0. 66 exit 0 first run. node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 (shallow clone lacked pinned fixture 621a487) → exit 0 after git fetch --depth=1 origin 621a4876. pnpm check:dts-closure exit 1 naming @objectstack/metadata-core d.ts missing while my own concurrent dogfood-closure build was rebuilding it → exit 0 on re-run after that build. NOT MEASURED locally (exit 3 prerequisite): pnpm check:dual-build-cjs-loads (39 packages without dist in this tree). NOT MEASURED locally: pnpm check:type-check-debt first exit 3 (its closure build timed out waiting on the verify lock my dogfood run held); a re-run under the lock was still measuring at report time. CI owns it as Type Check · debt ledger. Outside the derivation, asked by the seat: pnpm lint (eslint . --no-inline-config) exit 0, no output. node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD exit 0 (2 commits). CI on PR #25927 head d151a98 at report time: Dogfood Regression Gate success, Auto Label success, Test Core 1-8/8, Build Core, Type Check (consumer/source/debt ledger) and the Lint lanes in_progress; UI lanes, Temporal Conformance, Build Docs, opt-in harnesses skipped by path selection. CI convergence is the seat's read.",
    "dogfood_battery": "d151a987 · os-verify-lock VERDICT command-exit 0 (held 1444s) · pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test · Test Files 296 passed | 3 skipped (299) · Tests 2523 passed | 18 skipped (2541) · no failures",
    "mcp_calls": "9 — GitHub MCP reads only: issue_read get x2, issue_read get_comments x4, search_pull_requests x1, pull_request_read get x1, pull_request_read get_check_runs x1; no write tool. Plus harness subscribe_pr_activity on #25927 (not a GitHub write).",
    "api_writes": "3 relay strokes as objectstack-fleet[bot]: pr_create (run 40268483842: POST /repos/objectstack-ai/objectstack/pulls → #25927 draft, POST /issues/25927/assignees os-elon-musk; read-back 8667/8667 bytes identical), pr_request_reviewers (run 40268538127: POST /pulls/25927/requested_reviewers os-elon-musk, the project rule to request the requester's review), comment (this os-dev-report, POST /issues/25824/comments). git push x2 to claude/project-thread-8sle7x (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: NOT measured live (code reading only: admin-import-users.ts calls generateTemporaryPassword() with no length and its deps carry no auth context; better-auth createUser checks only the maximum) · on a deployment with minPasswordLength above 16, POST /api/v1/auth/admin/import-users with passwordPolicy temporary would save 16-character credentials below the minimum, the same defect class as this card, in a file outside this claim's surface · dedupe: "import-users temporary password minPasswordLength", "generateTemporaryPassword length import" · noted in PR Acceptance notes, not filed",
    "carrier: none · noted, not filed — the admin doors answer INVALID_REQUEST for a length refusal while better-auth sign-up/change-password answer PASSWORD_TOO_SHORT / PASSWORD_TOO_LONG; aligning them changes a wire answer, kept per triage "with the same code""
    ]
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25927 comment 6408577725 · domain:server seat 2 · session session_01TFnDdepdwyDJeM2kJ4GtMy · 2026-10-26T04:57Z · contract review PASS 6408288081 · sibling finding filed as #25932


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions