feat(queue): Piece D, reviews as issue comments, raw per-seat token helper (row 12, #1508)
queue move ID in-review posts the review request as a Gitea comment and review record reads verdicts back, so reviews stop being files in docs/plans/reviews/. On a comment round, in-review to waiting-on-jason now needs every listed reviewer's approval for the current round, the same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads the raw per-seat token files (lead decisions 37 to 39): config built and checked before curl starts, export attribute cleared, fixed base URL. test-queue.sh skips its live checks outside the canonical root. Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302) and corrected the plan (293747cd). Rocko reviewed the helper (e896192f, 2096b0a3), and Sage's lead check passed under decision 38. Manifest b402fb38, 19 files. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Gitea raw-token helper — R1 security review
|
||||
|
||||
Verdict: **changes requested**, one blocking finding. Round 1 of two.
|
||||
|
||||
Reviewed `helper.patch`, sha256 `c5e3de137ee999620688fab6b9721fe973a9d0ab1ca98ae6bcd8cc742d5620ee`, applied alone to an isolated shared clone at `8efc0ff3`. The patch hash was checked before reading and again after testing. Scope is lead decision 37's credential-handling helper; Filbert reviews Piece D. `scripts/mosaic queue next rocko` returned `nothing`; this explicit review request supplies the assignment.
|
||||
|
||||
## 1. Blocking — second validation failure does not prevent the request
|
||||
|
||||
Candidate `scripts/gitea-api.sh:86` and `:90` invoke curl with `<(gen_curl_cfg)`. Bash does not propagate the producer's exit status to curl. Revalidating in `cfg` mode is useful, but a refusal produces an empty config stream while curl still sends the request. Lines 99–100 can then report success on a 2xx response. A 401 is not guaranteed and is not a substitute for refusing before the request.
|
||||
|
||||
Independent deterministic reproduction used only dummy credentials and stub git/curl:
|
||||
|
||||
1. The first read accepts a valid raw dummy token.
|
||||
2. The git stub, which runs between the two reads, replaces that dummy file's contents with `invalid-token`.
|
||||
3. The second validator exits 3. The curl stub still runs, observes zero config bytes, and returns 200.
|
||||
4. The helper exits 0, prints `{}` and `HTTP 200`.
|
||||
|
||||
This control-flow hole predates the patch, as helper.md correctly discloses. It directly contradicts decision 37's acceptance condition for this new raw path: invalid content must refuse **before any request**. It also defeats the newly added second-read file checks at the request boundary. It needs fixing in this patch rather than deferral.
|
||||
|
||||
**Required change:** finish credential validation/config generation and check its success before starting curl. Keep the successful config in memory and deliver the token only through the config stream, never through argv, exported environment, disk or diagnostics. Preserve the JSON validation contract. Cover both body and no-body curl branches.
|
||||
|
||||
**Required regressions:** change a valid dummy file to invalid content, a symlink and a disallowed mode between the reads (or at the equivalent validation boundary after refactoring). Assert exit 3 and zero curl invocations, including a POST with a dummy body. Do not merely assert an eventual HTTP failure. Remove the success gate as a mutation and require these tests to fail.
|
||||
|
||||
## 2. Nonblocking — predictable response path is a separate existing issue
|
||||
|
||||
Lines 86/90/95/96 use `/tmp/gitea-api-response.$$`. A pre-existing symlink can redirect curl's output and the later `cat`; predictable shared-directory names are not a safe temporary-file allocation. This does not put the raw token in that file, and this patch does not introduce the path. I would keep it out of the narrow credential patch and track a separate fix using an exclusively created private temporary response file and cleanup on all exits. No exploit against a real path was attempted.
|
||||
|
||||
## What is fine
|
||||
|
||||
- The raw branch is reached only after JSON syntax failure. JSON values such as `null` and 40 decimal digits do not become raw credentials. The JSON URL/token validation rules remain unchanged.
|
||||
- The size, byte-count and ASCII pattern checks together accept exactly 40 lowercase hex characters with at most one final LF. Rejected contents emit no token. The fixed raw URL has no environment override.
|
||||
- Both reads perform the existing regular-file, no-symlink and group/other-mode checks. This is an improvement over the old second read, subject to finding 1. It is not an atomic file-descriptor identity guarantee across `lstat` and `readFile`; that separate pre-existing race is not claimed closed here.
|
||||
- On successful validation, the raw token reaches curl through its config descriptor and not through argv or normal stdout/stderr. Tests use dummy files and stub executables.
|
||||
- The helper is intentionally generic; it does not enforce the acting seat's identity. Decision 37's own-file rule remains an integration gate in Piece D (`credCheck` and `GET user` identity verification). This helper review does not certify those separate paths or authorize another seat's credentials.
|
||||
- Mutation results are honestly separated into killed and surviving mutations. The byte-length mutation is equivalent only for a stable file; helper.md already notes the changing-file exception. The missing request-gating test above is material despite the existing suite passing.
|
||||
|
||||
## Validation
|
||||
|
||||
`node --test packages/ledger/tests/` in the isolated candidate: **56/56 passed**, no skips. The separate between-read replacement reproducer demonstrated finding 1 with helper exit 0 and an empty config received by stub curl. No real credential file was opened, printed or tested; no live API request, source edit in the shared checkout, commit or restart was made. Only this review report was added to the checkout.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Gitea raw-token helper — final R2 review
|
||||
|
||||
Verdict: **changes requested**, one blocking finding. This is round 2 of 2; escalate the remaining blocker to Sage under item 27, not an automatic third round.
|
||||
|
||||
Target: `helper.patch`, sha256 `dd9e38bfa95d2e2305ad004a8583646bb90d6a5bf85cadf3140b3b688319770f`, applied alone to an isolated clone at `8efc0ff3`. Hash verified before reading and again after testing. Queue lookup returned `nothing`; the explicit review request supplies this assignment.
|
||||
|
||||
## 1. Blocking — inherited CFG retains its export attribute
|
||||
|
||||
Candidate `scripts/gitea-api.sh:74` assigns the secret configuration to `CFG` without clearing an inherited export attribute. In Bash, assigning a value to a variable imported from the environment does not make it private. The line-72 comment that CFG is not exported is therefore false when the caller already has an exported variable with that name.
|
||||
|
||||
Independent reproduction, with a dummy token and stub git/curl only:
|
||||
|
||||
- Start the helper with environment `CFG=innocent inherited value`.
|
||||
- Both validations pass, and line 74 replaces CFG with the credential config.
|
||||
- Stub curl observes the dummy token in its environment. The helper exits 0.
|
||||
|
||||
No caller needs to know or supply the credential value for this to happen. The body branch also starts external utilities after the assignment, so exposure is not limited to curl. This is newly introduced by storing the secret in a shell variable and violates decision 37's config-stream-only boundary.
|
||||
|
||||
**Required fix:** explicitly remove CFG's export attribute before launching any child with the captured secret, rather than relying on the absence of an `export CFG` statement. For example, a checked assignment followed immediately by Bash's builtin `export -n CFG`, before body-file creation or curl, addresses this inherited-attribute case. A task-specific name reduces collisions but does not replace clearing the attribute.
|
||||
|
||||
**Regression:** seed the helper environment with an exported, harmless CFG value. Exercise GET and POST using dummy raw and JSON credentials; assert that the token appears only in the config stream, never in child environment, argv or diagnostics. The current environment assertion runs without this seed, so it passes despite the bug. Removing the explicit attribute-clearing step must fail the regression.
|
||||
|
||||
## R1 disposition
|
||||
|
||||
The R1 blocker is **closed**. Configuration generation finishes synchronously and its failure prevents curl from starting in both branches. The added tests cover invalid text, invalid JSON shape, disallowed mode, symlink and missing-file changes between reads, plus a JSON-to-invalid transition. They assert exit 3 and zero curl calls rather than depending on an HTTP error. The nonempty check provides an additional guard.
|
||||
|
||||
The raw parser, fixed base URL and JSON validation rules retain the R1 behavior. The predictable response path remains a separate, acknowledged DEFERRED item and is not a blocker for this patch. Acting-seat path/login enforcement remains Piece D's integration responsibility, not certified by this helper-only review.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
The isolated R2 ledger suite passed **57/57**, no skips. Separately reproduced the inherited-export leak with the actual candidate helper and stubs, reporting only the boolean result, not credential contents. A small shell check confirmed that `export -n CFG` removes the inherited export attribute. No candidate source was changed for this review, and no real token file was opened or printed. No live API call, commit, push or restart occurred. Only this report was written in the shared checkout.
|
||||
Reference in New Issue
Block a user