fix(#3): Harden ops security — auth /update_terms, ADMIN_TOKEN, redirect allowlist, trusted XFF #22

Merged
PlasticDigits merged 3 commits from fix/issue-3-security-ops into main 2026-08-10 02:19:24 +00:00
PlasticDigits commented 2026-08-10 00:53:59 +00:00 (Migrated from gitlab.com)

Summary

Closes the portal/API ops security bundle from issue #3 / gap items 4–7:

  • Authenticate POST /update_terms with ADMIN_TOKEN Bearer (POST-only; auth before 1 req/s limit)
  • Fail-fast ADMIN_TOKEN (no silent dev-admin-token; ALLOW_INSECURE_DEFAULTS local opt-in)
  • Constant-time Bearer compare (subtle) shared by admin + update_terms
  • Portal redirect_uri allowlist (VITE_REDIRECT_URI_ALLOWLIST / localhost flag); SDK sanitizeRedirectUri
  • Rate-limit IP uses TCP peer by default; TRUSTED_PROXY_CIDRS enables XFF (rightmost hop)
  • GET /health for readiness (e2e/CI); docs + skills/security-ops/SKILL.md

Test plan

  • cd api && cargo test (unit + integration auth)
  • SDK + web Vitest (redirect allowlist)
  • Playwright e2e (authenticated terms sync + EVM sign) — 12 passed
  • Manual curl: unauth/bad Bearer → 401; valid Bearer → sync; /health → ok
  • Staging: confirm prod ADMIN_TOKEN set; TRUSTED_PROXY_CIDRS only if behind reverse proxy; set VITE_REDIRECT_URI_ALLOWLIST for real dapp origins

Acceptance criteria (issue #3)

  • Unauthenticated /update_terms → 401; authenticated still syncs
  • No known default admin token in production config; CI/dev documented
  • Portal refuses non-allowlisted redirect_uri
  • XFF spoofing ignored without trusted proxies; trusted mode documented + tested
  • Admin bearer comparison constant-time
  • README + .env.example + CI/e2e updated
  • EVM Playwright e2e green with authenticated terms sync

Closes #3

## Summary Closes the portal/API ops security bundle from issue #3 / gap items 4–7: - Authenticate `POST /update_terms` with `ADMIN_TOKEN` Bearer (POST-only; auth before 1 req/s limit) - Fail-fast `ADMIN_TOKEN` (no silent `dev-admin-token`; `ALLOW_INSECURE_DEFAULTS` local opt-in) - Constant-time Bearer compare (`subtle`) shared by admin + update_terms - Portal `redirect_uri` allowlist (`VITE_REDIRECT_URI_ALLOWLIST` / localhost flag); SDK `sanitizeRedirectUri` - Rate-limit IP uses TCP peer by default; `TRUSTED_PROXY_CIDRS` enables XFF (rightmost hop) - `GET /health` for readiness (e2e/CI); docs + `skills/security-ops/SKILL.md` ## Test plan - [x] `cd api && cargo test` (unit + integration auth) - [x] SDK + web Vitest (redirect allowlist) - [x] Playwright e2e (authenticated terms sync + EVM sign) — 12 passed - [x] Manual curl: unauth/bad Bearer → 401; valid Bearer → sync; `/health` → ok - [ ] Staging: confirm prod `ADMIN_TOKEN` set; `TRUSTED_PROXY_CIDRS` only if behind reverse proxy; set `VITE_REDIRECT_URI_ALLOWLIST` for real dapp origins ## Acceptance criteria (issue #3) - [x] Unauthenticated `/update_terms` → 401; authenticated still syncs - [x] No known default admin token in production config; CI/dev documented - [x] Portal refuses non-allowlisted `redirect_uri` - [x] XFF spoofing ignored without trusted proxies; trusted mode documented + tested - [x] Admin bearer comparison constant-time - [x] README + `.env.example` + CI/e2e updated - [x] EVM Playwright e2e green with authenticated terms sync Closes #3
PlasticDigits commented 2026-08-10 01:07:56 +00:00 (Migrated from gitlab.com)

RECOMMEND: ACCEPT

Thorough security + gap review of MR !22 (fix/issue-3-security-ops → main, c40ea6e) against issue #3 / GAP items 4–7.

Verdict

Issue #3 acceptance criteria are met in code. No critical/high defects or prompt-injection issues in the changed surface. Residual items are configuration hardening and test-depth follow-ups — not merge blockers for this scoped ops-security bundle.

Out of scope / no regression from this diff: DeFi attacks, smart-contract attacks, oracle manipulation, tokenomic/economic attacks (no on-chain contracts/oracles/tokenomics in repo or this MR). Pre-existing out-of-scope debt (Telegram HMAC, Solana/Terra verify, bot fail-open, open property auto-upsert) remains unchanged.


Issue #3 acceptance criteria

Criterion Status Evidence
Unauthenticated /update_terms → 401; authenticated syncs Met api/src/routes/update_terms.rs (POST-only, require_admin before rate limit + sync); deny paths in api/tests/integration_test.rs
No known default admin token in prod config Met api/src/config.rs resolve_admin_token — missing/empty/dev-admin-token fail-fast unless ALLOW_INSECURE_DEFAULTS
Portal refuses non-allowlisted redirect_uri Met web/src/ui.ts + sanitizeRedirectUri (SDK + portal)
XFF spoofing ignored without trusted proxies Met api/src/rate_limit.rs peer default; TRUSTED_PROXY_CIDRS + rightmost hop
Admin bearer compare constant-time Met api/src/auth.rs (subtle::ConstantTimeEq); shared by admin + update_terms
README + .env.example + CI/e2e updated Met docs, env examples, .gitlab-ci.yml, Playwright ADMIN_TOKEN + /health
EVM Playwright e2e green with authenticated sync Met (per MR; global-setup sends Bearer) Staging checklist item still open (expected)

GAP items 4–7 marked Addressed in gaps/GAP_1786322222.md — accurate for portal/API ops.


Security audit (changed code)

Area Result
Access control / privileges /update_terms + /admin/* gated on shared Bearer; auth before ops rate limit (good)
Open redirect / phishing Scheme allowlist; rejects javascript:, data:, //, userinfo; origin match; deny-by-default when allowlist empty
XFF / rate-limit bypass Default safe (peer only); trusted-proxy mode documented
Timing oracle on Bearer Constant-time compare centralized
CSRF GET removed; Bearer not cookie-based
DB leaks / SQLi No schema change; parameterized queries unchanged; auth bypass to admin data closed for this path
/health info leak Minimal {status: ok} — OK for probes
SSRF via sync URL Not request-controlled (config-only); pre-existing if env compromised
Prompt injection (skills/docs) Operational docs only — no finding

Residual (non-blocking)

Severity Finding Suggested follow-up
Medium .env.example ships ADMIN_TOKEN=change-me-admin-token; fail-fast only blocks dev-admin-token Also reject known example placeholders, or leave example commented/empty
Medium TRUSTED_PROXY_CIDRS accepts 0.0.0.0/0 / ::/0 with no boot guard Reject world-trust CIDRs at parse time
Medium No Playwright e2e for allowlisted vs evil redirect_uri Add browser cases
Medium No integration happy-path for authenticated /update_terms → published/unchanged Add assertion
Medium Blocked redirect: success UI with no “redirect not allowed” hint Operator/UX polish
Low GAP §5 still claims missing admin/XFF tests (stale vs new unit tests) Doc cleanup
Low Admin DELETE + cascade not integration-tested Optional

Attack / test matrix (issue #3)

Vector Expectation Code Tests
Unauth flood /update_terms 401; no GitLab/publish ✅ Partial (401 covered; no outbound-fetch assertion)
Default/guessable admin token Fail-fast ✅ (dev-admin-token) Unit ✅; example placeholder residual
redirect_uri=https://evil.com No navigate ✅ Vitest ✅; e2e ❌
javascript: / data: / // / userinfo Blocked ✅ Vitest ✅
XFF spoof without trust Ignored ✅ Unit ✅; HTTP integration ❌
Bearer timing Constant-time ✅ Unit equality only
Admin DELETE cascade abuse Still needs token ✅ (unchanged) Not covered

Local test signal (review env): API unit tests pass; SDK Vitest 21/21; web Vitest 13/13; one unrelated integration fixture flake (version_label=1.5 duplicate) — not introduced as a security regression by this MR. Full Playwright not re-run here.


Gap analysis (features / packages / quality)

Package Assessment
api/ Coherent: new auth.rs, fail-fast config, POST-only update_terms, XFF policy, /health
web/ Redirect sanitize at navigate; e2e setup authenticated
packages/cl8y-clickwrap Shared sanitizeRedirectUri; optional for headless integrators (documented)
scripts/, CI, README, skills/security-ops Aligned
bot/, contracts Untouched (correct)
Gas / DRY / readability Gas N/A; DRY improved (shared auth + redirect); Rust/TS patterns idiomatic

User/operator expectations still open (follow-ups): blocked-redirect messaging; /ready (DB) vs liveness-only /health; state round-trip on redirect (GAP #13); staging env confirmation (MR checklist).


Bottom line

Accept — closes the intended control-plane / open-redirect / XFF / default-token bundle for issue #3 with sound design and adequate automated coverage for deny paths. Please track the medium follow-ups (example-token denylist, world-CIDR guard, redirect e2e + sync happy-path test) as non-blocking hardening.

RECOMMEND: ACCEPT Thorough security + gap review of MR !22 (`fix/issue-3-security-ops` → `main`, `c40ea6e`) against issue #3 / GAP items 4–7. ## Verdict Issue #3 acceptance criteria are **met in code**. No critical/high defects or prompt-injection issues in the changed surface. Residual items are configuration hardening and test-depth follow-ups — not merge blockers for this scoped ops-security bundle. **Out of scope / no regression from this diff:** DeFi attacks, smart-contract attacks, oracle manipulation, tokenomic/economic attacks (no on-chain contracts/oracles/tokenomics in repo or this MR). Pre-existing out-of-scope debt (Telegram HMAC, Solana/Terra verify, bot fail-open, open property auto-upsert) remains unchanged. --- ## Issue #3 acceptance criteria | Criterion | Status | Evidence | |-----------|--------|----------| | Unauthenticated `/update_terms` → 401; authenticated syncs | **Met** | `api/src/routes/update_terms.rs` (POST-only, `require_admin` before rate limit + sync); deny paths in `api/tests/integration_test.rs` | | No known default admin token in prod config | **Met** | `api/src/config.rs` `resolve_admin_token` — missing/empty/`dev-admin-token` fail-fast unless `ALLOW_INSECURE_DEFAULTS` | | Portal refuses non-allowlisted `redirect_uri` | **Met** | `web/src/ui.ts` + `sanitizeRedirectUri` (SDK + portal) | | XFF spoofing ignored without trusted proxies | **Met** | `api/src/rate_limit.rs` peer default; `TRUSTED_PROXY_CIDRS` + rightmost hop | | Admin bearer compare constant-time | **Met** | `api/src/auth.rs` (`subtle::ConstantTimeEq`); shared by admin + update_terms | | README + `.env.example` + CI/e2e updated | **Met** | docs, env examples, `.gitlab-ci.yml`, Playwright `ADMIN_TOKEN` + `/health` | | EVM Playwright e2e green with authenticated sync | **Met** (per MR; global-setup sends Bearer) | Staging checklist item still open (expected) | GAP items 4–7 marked **Addressed** in `gaps/GAP_1786322222.md` — accurate for portal/API ops. --- ## Security audit (changed code) | Area | Result | |------|--------| | Access control / privileges | `/update_terms` + `/admin/*` gated on shared Bearer; auth before ops rate limit (good) | | Open redirect / phishing | Scheme allowlist; rejects `javascript:`, `data:`, `//`, userinfo; origin match; deny-by-default when allowlist empty | | XFF / rate-limit bypass | Default safe (peer only); trusted-proxy mode documented | | Timing oracle on Bearer | Constant-time compare centralized | | CSRF | GET removed; Bearer not cookie-based | | DB leaks / SQLi | No schema change; parameterized queries unchanged; auth bypass to admin data closed for this path | | `/health` info leak | Minimal `{status: ok}` — OK for probes | | SSRF via sync URL | Not request-controlled (config-only); pre-existing if env compromised | | Prompt injection (skills/docs) | Operational docs only — no finding | ### Residual (non-blocking) | Severity | Finding | Suggested follow-up | |----------|---------|---------------------| | Medium | `.env.example` ships `ADMIN_TOKEN=change-me-admin-token`; fail-fast only blocks `dev-admin-token` | Also reject known example placeholders, or leave example commented/empty | | Medium | `TRUSTED_PROXY_CIDRS` accepts `0.0.0.0/0` / `::/0` with no boot guard | Reject world-trust CIDRs at parse time | | Medium | No Playwright e2e for allowlisted vs evil `redirect_uri` | Add browser cases | | Medium | No integration happy-path for authenticated `/update_terms` → `published`/`unchanged` | Add assertion | | Medium | Blocked redirect: success UI with no “redirect not allowed” hint | Operator/UX polish | | Low | GAP §5 still claims missing admin/XFF tests (stale vs new unit tests) | Doc cleanup | | Low | Admin DELETE + cascade not integration-tested | Optional | --- ## Attack / test matrix (issue #3) | Vector | Expectation | Code | Tests | |--------|-------------|------|-------| | Unauth flood `/update_terms` | 401; no GitLab/publish | ✅ | Partial (401 covered; no outbound-fetch assertion) | | Default/guessable admin token | Fail-fast | ✅ (`dev-admin-token`) | Unit ✅; example placeholder residual | | `redirect_uri=https://evil.com` | No navigate | ✅ | Vitest ✅; e2e ❌ | | `javascript:` / `data:` / `//` / userinfo | Blocked | ✅ | Vitest ✅ | | XFF spoof without trust | Ignored | ✅ | Unit ✅; HTTP integration ❌ | | Bearer timing | Constant-time | ✅ | Unit equality only | | Admin DELETE cascade abuse | Still needs token | ✅ (unchanged) | Not covered | **Local test signal (review env):** API unit tests pass; SDK Vitest 21/21; web Vitest 13/13; one unrelated integration fixture flake (`version_label=1.5` duplicate) — not introduced as a security regression by this MR. Full Playwright not re-run here. --- ## Gap analysis (features / packages / quality) | Package | Assessment | |---------|------------| | `api/` | Coherent: new `auth.rs`, fail-fast config, POST-only update_terms, XFF policy, `/health` | | `web/` | Redirect sanitize at navigate; e2e setup authenticated | | `packages/cl8y-clickwrap` | Shared `sanitizeRedirectUri`; optional for headless integrators (documented) | | `scripts/`, CI, README, `skills/security-ops` | Aligned | | `bot/`, contracts | Untouched (correct) | | Gas / DRY / readability | Gas N/A; DRY improved (shared auth + redirect); Rust/TS patterns idiomatic | **User/operator expectations still open (follow-ups):** blocked-redirect messaging; `/ready` (DB) vs liveness-only `/health`; `state` round-trip on redirect (GAP #13); staging env confirmation (MR checklist). --- ## Bottom line **Accept** — closes the intended control-plane / open-redirect / XFF / default-token bundle for issue #3 with sound design and adequate automated coverage for deny paths. Please track the medium follow-ups (example-token denylist, world-CIDR guard, redirect e2e + sync happy-path test) as non-blocking hardening.
PlasticDigits commented 2026-08-10 01:54:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #3

mentioned in issue #3
PlasticDigits commented 2026-08-10 01:59:22 +00:00 (Migrated from gitlab.com)

added 5 commits

  • 74a80e65 - Fix Terra Classic Keplr signing with ADR-036 verify.
  • 62d8d76c - Harden Terra ADR-036 CI, abuse tests, and portal invariants.
  • 6ec0af69 - Stabilize Playwright e2e startup on CI.
  • 16a75c63 - Harden portal/API ops security for issue #3.
  • f5c490ff - Fix integration test Config after rebase onto Terra ADR-036.

Compare with previous version

added 5 commits <ul><li>74a80e65 - Fix Terra Classic Keplr signing with ADR-036 verify.</li><li>62d8d76c - Harden Terra ADR-036 CI, abuse tests, and portal invariants.</li><li>6ec0af69 - Stabilize Playwright e2e startup on CI.</li><li>16a75c63 - Harden portal/API ops security for issue #3.</li><li>f5c490ff - Fix integration test Config after rebase onto Terra ADR-036.</li></ul> [Compare with previous version](/PlasticDigits/cl8y-ecosystem-legal/-/merge_requests/4/diffs?diff_id=1951270873&start_sha=c40ea6e7b5d7a5273e435413c0b345bf40e6f83f)
PlasticDigits commented 2026-08-10 02:04:36 +00:00 (Migrated from gitlab.com)

added 4 commits

  • 03849d3b - 1 commit from branch main
  • b0725585 - Harden portal/API ops security for issue #3.
  • 8294413c - Fix integration test Config after rebase onto Terra ADR-036.
  • c5fdf1a3 - Remove unused PrehashSigner import in integration tests.

Compare with previous version

added 4 commits <ul><li>03849d3b - 1 commit from branch <code>main</code></li><li>b0725585 - Harden portal/API ops security for issue #3.</li><li>8294413c - Fix integration test Config after rebase onto Terra ADR-036.</li><li>c5fdf1a3 - Remove unused PrehashSigner import in integration tests.</li></ul> [Compare with previous version](/PlasticDigits/cl8y-ecosystem-legal/-/merge_requests/4/diffs?diff_id=1951273414&start_sha=f5c490ffae17ea288589fedd022f7971adc08472)
PlasticDigits (Migrated from gitlab.com) scheduled this pull request to auto merge when all checks succeed 2026-08-10 02:09:48 +00:00
PlasticDigits commented 2026-08-10 02:15:16 +00:00 (Migrated from gitlab.com)

mentioned in issue #2

mentioned in issue #2
PlasticDigits commented 2026-08-10 02:18:37 +00:00 (Migrated from gitlab.com)

aborted the automatic merge because merge request was closed

aborted the automatic merge because merge request was closed
PlasticDigits commented 2026-08-10 02:19:24 +00:00 (Migrated from gitlab.com)

mentioned in commit 8cf6527707

mentioned in commit 8cf6527707947dbdecf40f9edd7df0015b90bccc
PlasticDigits (Migrated from gitlab.com) merged commit 8cf6527707 into main 2026-08-10 02:19:24 +00:00
PlasticDigits commented 2026-08-10 02:19:28 +00:00 (Migrated from gitlab.com)

Merged to main via git merge (tip c5fdf1a) because glab mr merge was blocked by CI quota (ci_quota_exceeded / ci_must_pass after canceled pipeline). Commit is on main.

Merged to `main` via git merge (tip `c5fdf1a`) because `glab mr merge` was blocked by CI quota (`ci_quota_exceeded` / `ci_must_pass` after canceled pipeline). Commit is on `main`.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-ecosystem-legal!22
No description provided.