修复认证挑战与可信代理安全边界(07-17-auth-challenge-trusted-proxy-hardening)
这个提交包含在:
@@ -294,3 +294,92 @@ await buildApp(); // CJS plugin may still construct the real client
|
||||
```js
|
||||
await buildApp({ prismaPlugin: testPrismaPlugin, startJobs: noOpStartJobs });
|
||||
```
|
||||
|
||||
## Scenario: Authentication Challenge Serialization
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing password reset, email-code login, refresh rotation,
|
||||
password login session creation, user password change or administrator reset.
|
||||
- Applies when a credential event can consume a challenge, change
|
||||
`token_version`, create/revoke a refresh session or invalidate another
|
||||
challenge.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Row barrier: `SELECT ... FROM employees WHERE ... FOR UPDATE`.
|
||||
- Challenge rows: `password_reset_codes` and `email_login_codes`.
|
||||
- Session rows: `auth_sessions`.
|
||||
- Security epoch: `employees.token_version`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Use one lock order for credential mutations: Employee first, then challenge
|
||||
and session rows. Refresh rotation must lock Employee and re-read the session
|
||||
inside the same transaction before rotating it.
|
||||
- Capture expiry, cooldown and lockout time after the Employee lock is acquired;
|
||||
a request queued behind another credential event must not use stale time.
|
||||
- Creating a new challenge invalidates the previous unused challenge while the
|
||||
Employee lock is held. An active challenge with a wrong code increments the
|
||||
account-level failure state; no active challenge returns the generic failure
|
||||
without penalizing concurrent correct-request losers.
|
||||
- Successful reset, user password change and administrator reset increment
|
||||
`token_version`, revoke active refresh sessions and invalidate unused
|
||||
challenges in the same transaction as the password change and audit event.
|
||||
- Pepper-backed hashes use distinct purpose domains for email login and
|
||||
password reset. Production rejects a missing, placeholder, weak or
|
||||
JWT-reused pepper.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Unknown/inactive/locked account or missing/expired/consumed challenge ->
|
||||
stable generic response; no account enumeration detail.
|
||||
- Wrong code for the current active challenge -> increment account and
|
||||
challenge attempts; reaching the bound locks and invalidates the challenge.
|
||||
- Reset commits before login/refresh -> the queued request rechecks state and
|
||||
cannot create a usable session.
|
||||
- Login/refresh commits before reset -> reset observes and revokes the newly
|
||||
created session before it commits.
|
||||
- Concurrent consumers of one valid challenge -> exactly one successful state
|
||||
transition; losers do not create audits, sessions or a recovery lock.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: every credential path uses Employee -> challenge/session ordering and
|
||||
is covered by a deterministic PostgreSQL lock race.
|
||||
- Base: sequential invalid-code attempts use the same account-level counters
|
||||
and return the same external error contract.
|
||||
- Bad: refresh locks a session before Employee while reset locks Employee
|
||||
before sessions; this can leave a newly inserted session outside reset's
|
||||
statement snapshot.
|
||||
- Bad: treat `!challenge` after another request consumed the code as a new
|
||||
failed guess and lock the legitimate account.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Focused route tests for CSPRNG/HMAC domain separation, cooldown, account
|
||||
lockout, missing-challenge losers, conditional consumption and session
|
||||
revocation.
|
||||
- Real isolated PostgreSQL checks for concurrent challenge requests, 20-way
|
||||
same-code consumption, reset-first/login-first ordering and refresh/reset.
|
||||
- After reset, assert the pre-reset access token and refresh cookie both fail,
|
||||
the active-session count is zero and only one success audit exists.
|
||||
- Prisma validate plus clean migration deployment from an empty database.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
await tx.authSession.updateMany({ where: { id: sessionId, revoked_at: null } });
|
||||
await tx.authSession.create({ data: replacement });
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
await lockEmployeeForUpdate(tx, employeeId);
|
||||
const currentSession = await tx.authSession.findUnique({ where: { id: sessionId } });
|
||||
if (!currentSession || currentSession.revoked_at) return invalidRefresh();
|
||||
await rotateSession(tx, currentSession);
|
||||
```
|
||||
|
||||
@@ -352,3 +352,93 @@ node deploy/trialrun/check-source-provenance.mjs
|
||||
BUILD_SHA=<verified-full-sha> VITE_BUILD_SHA=<matching-short-sha> \
|
||||
docker compose -f deploy/trialrun/docker-compose.yml build server
|
||||
```
|
||||
|
||||
## Scenario: Isolated Authentication Security Gate
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing authentication challenges, credential/session transactions,
|
||||
trusted proxy handling or the deployment order for their schema migration.
|
||||
- The gate proves local PostgreSQL behavior only; public deployment and edge
|
||||
changes remain separately approved operations.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Complete local gate: `bash scripts/qa/run-web-gates.sh auth-security-local`.
|
||||
- Database allowlist: loopback PostgreSQL and a fresh `hyb_oa_auth_*` database
|
||||
whose exact name is confirmed to the checker.
|
||||
- Runtime guard: `RUN_AUTH_SECURITY_REGRESSION=1`; concurrency mode accepts only
|
||||
`0` or `1` and reports `FULL` only for `1`.
|
||||
- Public release/rollback gate: four challenge endpoints marked
|
||||
`EXTERNAL_GATED` in the runbook and checklist.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Validate loopback API, loopback PostgreSQL, isolated database name, explicit
|
||||
confirmation and test mode before the first HTTP write.
|
||||
- The authentication gate generates per-run JWT, pepper and field-encryption
|
||||
test keys; it must not inherit production values. Email, IM, WeCom and OIDC
|
||||
side effects are disabled or injected with a no-network test service.
|
||||
- PID/log paths are unique per run. Cleanup may stop only the server spawned by
|
||||
the current run, and a database drop failure makes the gate fail.
|
||||
- The runner removes only a database that this invocation successfully created;
|
||||
failed creation must never authorize deletion of an existing database.
|
||||
- Deployment or rollback to old challenge code requires an operator to close
|
||||
`/api/auth/forgot-password`, `/api/auth/reset-password`,
|
||||
`/api/auth/email-login/code` and `/api/auth/email-login/verify`. Scripts do
|
||||
not claim this external action occurred.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Remote API/DB, wrong database prefix/confirmation or production mode -> fail
|
||||
before login, seed-user mutation or challenge requests.
|
||||
- Code-display logging, enabled external adapter or inherited proxy hop -> fail
|
||||
the test-server guard.
|
||||
- Concurrency value other than `0|1` -> fail; `0` -> `BASE_ONLY/PARTIAL`; `1`
|
||||
plus all PostgreSQL scenarios -> `FULL`.
|
||||
- Owned server cleanup failure or temporary database drop failure -> non-zero
|
||||
gate result.
|
||||
- Four-endpoint edge confirmation absent -> public release/rollback remains
|
||||
`EXTERNAL_GATED`, regardless of local test success.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: disposable loopback PostgreSQL, synthetic secrets, no-network auth
|
||||
email seam, five concurrency scenarios, zero residual databases and an
|
||||
automatically removed container.
|
||||
- Base: the HTTP regression runs without PostgreSQL concurrency and reports
|
||||
partial evidence rather than a complete security pass.
|
||||
- Bad: run several mutating HTTP steps and only then discover that the target
|
||||
database or API is remote.
|
||||
- Bad: reuse a shared PID file or ignore `dropdb` failure while returning exit
|
||||
code zero.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Static guard tests for remote authority, query-host override, exact database
|
||||
confirmation, secret isolation, external-adapter disablement, owned cleanup
|
||||
and redacted failure output.
|
||||
- `bash -n` for the runner and regression scripts; Node syntax checks for the
|
||||
checker and test-server entry.
|
||||
- Prisma validate, clean migration deployment, the full isolated gate and an
|
||||
explicit post-run query proving no `hyb_oa_auth_*` database remains.
|
||||
- `node deploy/trialrun/validate-env-contract.js` must enforce loopback API
|
||||
binding, XFF overwrite templates and four-endpoint `EXTERNAL_GATED` wording.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```bash
|
||||
run_mutating_auth_regression
|
||||
validate_target_afterwards
|
||||
dropdb --if-exists "$DB" || true
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```bash
|
||||
validate_isolated_target_before_writes
|
||||
run_mutating_auth_regression
|
||||
drop_owned_database_or_fail
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户