Skip to content

docs(audit): close SEC-08, SEC-09 and SEC-26 - #15

Merged
JOY (JOY) merged 1 commit into
devfrom
docs/audit-close-sec-08-09-26
Sep 13, 2026
Merged

JOY (JOY) merged 1 commit into
devfrom
docs/audit-close-sec-08-09-26

Conversation

@JOY

@JOY JOY (JOY) commented Sep 13, 2026

Copy link
Copy Markdown

Documentation only - one file. CI skips it because docs/ is in the paths-ignore list.

Marks the three IDs Done and records what shipped, in the dependency order that mattered: SEC-26 first, because SEC-09 and SEC-08 both key on a client address that was forgeable until it landed. Building them in the other order would have produced a control an attacker could lift off with one header.

  • SEC-26 (PR fix(security): make the client IP trustworthy and scope credential lockout to it #13) - trustedProxies and trustedPlatform applied in NewServer before any middleware. An unparseable CIDR fails startup rather than quietly dropping the trust boundary. The default is the loopback / RFC1918 / IPv6 unique-local / link-local ranges, not Gin's trust-everything default. trustedPlatform defaults to empty on purpose: an edge that appends instead of overwriting would hand the header straight back to the caller.
  • SEC-09 (PR fix(security): make the client IP trustworthy and scope credential lockout to it #13) - isCredentialLocked now takes the client address and applies two windows: principal-and-address for one source grinding on one account, and address-across-all-principals for credential stuffing. An address the server cannot determine normalises to one unknown bucket and is excluded from the per-address window, so a missing IP cannot pool every such caller into a single lockout.
  • SEC-08 (PR feat(security): rate limit the public endpoints an anonymous caller can drive #14) - six public routes limited. Channel webhooks, websockets, the HMAC webhooks and the whole authenticated dashboard are deliberately exempt, and a test fires 1000 requests at five of those routes and fails on any 429, because a platform that gets throttled on its webhook eventually disables the delivery and takes a channel offline.

Also recorded, because leaving these out would make the document read better than the work was

  • Retry-After shipped wrong at first. int(seconds)+1 returns 61 for a 60 second window whenever the clock has not ticked between requests - which on Windows means three requests in a row. My own test caught it; it now rounds up properly.
  • The limiter was never validated under -race. This repository builds with CGO disabled and go test -race requires cgo. The concurrency test still has teeth, because an unguarded concurrent map write panics rather than merely miscounting, but it is not a substitute for the race detector.
  • None of the three has been ported upstream. They cannot be replayed the way the upload fix was: internal/pkg/config/config.go on dev carries fork-only fields, and internal/bootstrap/server.go on dev already contains the storage hardening still sitting in the open upstream PR fix(storage): stop guest uploads from becoming same-origin script execution huabeitech/agent-desk#40. Copying either file across would ship fork configuration upstream and duplicate an open pull request.
  • The four pre-existing lockout tests failed when the key changed, because they seeded credential logs without a client address. They encoded the vulnerable semantics. That failure is the evidence the fix is real, not a regression to paper over.
  • A second git incident. A file showed as modified in git status while git diff was empty - a stat-cache artifact after gofmt -w then git restore - and it blocked a fast-forward merge. Resolved by proving the content identical with git diff --exit-code HEAD, then git add --renormalize, then confirming the staged diff was empty before continuing. No content byte changed.

Register arithmetic

Remaining moves from 89 to 86. All six P0 IDs are now addressed - four Done, two Partial, none open.

Every declared number in the document is cross-checked against the register tables by script: 18 prose and stat-card sites, plus the per-prefix stats table and its total row. The check passes, along with tag balance and a sweep for stale markers such as the previous remaining-count and the superseded Round 5 header.


Note

Low Risk
Single HTML audit file only; no runtime, auth, or deployment behavior changes.

Overview
Documentation-only update to docs/CROVE_DESK_AUDIT.html (v5 verification log + register rows). No application code; docs/** is in CI paths-ignore, so this PR does not run the test workflow.

The audit register now records SEC-26, SEC-09, and SEC-08 as Done, with closure tied to PR #13 (trusted proxy / client IP, then IP-aware credential lockout) and PR #14 (public-route rate limiting). Prose stresses dependency order: SEC-26 had to land before IP-based lockout and limits, or those controls could be bypassed via forged headers.

Counts and status narrative move from 21 → 24 closed IDs and 89 → 86 remaining; Round 5 header states SEC-26 was introduced and closed in the same round. The “what’s left” section now says all six P0 items are addressed (four Done, two Partial) and replaces “do SEC-26 next” with “that chain is closed,” listing follow-ups such as SEC-02 plaintext password, SEC-10/12/13, and vi-VN strings.

The v5 (2026-09-13) block in the verification log adds merge notes for PRs #12#14, the Retry-After ceiling fix, honest limits (no -race on the limiter, no upstream port yet), local/CI test results, a git stat-cache/CRLF merge unblock, and AGENTS.md rules via PR #10.

Reviewed by Cursor Bugbot for commit 31820b5. Configure here.

…e verified

Marks the three IDs Done and records what actually shipped, in the dependency
order that mattered: SEC-26 first, because the other two key on a client address
that was forgeable until it landed.

SEC-26 (PR #13). ServerConfig gains trustedProxies and trustedPlatform, applied
in NewServer before any middleware. An unparseable CIDR fails startup rather than
quietly dropping the trust boundary. The default is the loopback, RFC1918, IPv6
unique-local and link-local ranges rather than Gin's trust-everything default,
and trustedPlatform defaults to empty on purpose - an edge that appends instead of
overwriting would hand the header back to the caller.

SEC-09 (PR #13). isCredentialLocked now takes the client address and applies two
windows: principal-and-address for one source grinding on one account, and
address-across-all-principals for credential stuffing. The four existing lockout
tests failed when the key changed, because they seeded credential logs without a
client address - they encoded the vulnerable semantics. That failure is the
evidence the fix is real.

SEC-08 (PR #14). Six public routes limited, with the channel webhooks,
websockets, HMAC webhooks and the whole dashboard deliberately exempt; a test
fires 1000 requests at five of those routes and fails on any 429, because a
platform that gets throttled on its webhook eventually disables the delivery and
takes a channel offline.

Also recorded, because leaving them out would make the document read better than
the work was:

- Retry-After shipped as int(seconds)+1 at first, which returns 61 for a 60
  second window whenever the clock has not ticked between requests. My own test
  caught it.
- The limiter could not be validated under -race. This repository builds with CGO
  disabled and go test -race requires cgo. The concurrency test still has teeth,
  since an unguarded concurrent map write panics rather than merely miscounting,
  but it is not a substitute for the race detector.
- SEC-26, SEC-09 and SEC-08 have not been ported upstream. They cannot be replayed
  the way the upload fix was: config.go on dev carries fork-only fields, and
  server.go on dev already contains the storage hardening still sitting in the
  open upstream PR huabeitech#40.
- A second git incident. A file showed as modified in git status while git diff
  was empty - a stat-cache artifact after gofmt then restore - and it blocked a
  fast-forward. Resolved by proving the content identical with
  git diff --exit-code HEAD, then git add --renormalize, then confirming the
  staged diff was empty before continuing.

Remaining count moves from 89 to 86. Every declared number in the document is
cross-checked against the register tables by script across 18 sites plus the
per-prefix stats table and its total row; the check passes.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f5e5e36a-2d8c-4371-a885-b18aaf2b2ec2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_2cff6545-1bbb-4cac-9141-e31751a1fdd3)

@JOY
JOY (JOY) merged commit 17c4e6f into dev Sep 13, 2026
4 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the audit and issue register documentation in docs/CROVE_DESK_AUDIT.html to record the resolution of security issues SEC-08 (rate limiting), SEC-09 (credential lockout), and SEC-26 (trusted proxies). The feedback suggests a minor improvement to maintain language consistency in the Vietnamese documentation by replacing 'with' with 'với' in the CGO build description.

<tr><td class="id">SEC-07 <span class="badge b-good">Done</span></td><td><span class="badge b-high">High</span></td><td>WebSocket bypass permission <code>conversation.view</code>. REST gate ở 5 endpoint; WS trả <code>true</code> cho mọi admin-role session. Payload chứa <strong>full nội dung tin nhắn</strong>. ID tuần tự → enumerate được.</td><td class="loc">services/ws_service.go:624-628, 301-317 · vs handlers/dashboard/conversation_handler.go:24,75,108,127,287</td><td class="v v-y">✓</td></tr>
<tr><td class="id">SEC-08</td><td><span class="badge b-med">Med</span></td><td>Không có rate limiting ở bất kỳ đâu (grep 0 kết quả). Khuếch đại SEC-01 (flood 20MB), SEC-09, spam register, và DoS tốn tiền LLM qua flood tin nhắn.</td><td class="loc">toàn bộ internal/</td><td class="v v-y">✓</td></tr>
<tr><td class="id">SEC-09</td><td><span class="badge b-med">Med</span></td><td>Credential lockout key theo <strong>username, không theo IP</strong> → attacker biết username (vd <code>admin</code>) khóa tài khoản thật 15 phút, lặp vô hạn = DoS vĩnh viễn.</td><td class="loc">services/auth_service.go:465-479</td><td class="v v-y">✓</td></tr>
<tr><td class="id">SEC-08 <span class="badge b-good">Done</span></td><td><span class="badge b-med">Med</span></td><td>Không có rate limiting ở bất kỳ đâu (grep 0 kết quả). Khuếch đại SEC-01 (flood 20MB), SEC-09, spam register, và DoS tốn tiền LLM qua flood tin nhắn.<br><b>Đã đóng (PR #14, 2026-09-13).</b> Package <code>internal/pkg/ratelimit</code> fixed-window in-process + middleware, gắn vào 6 route public: login 20 · register 10 · session_exchange <b>120</b> (phải chịu được cả văn phòng sau một NAT) · upload attachment+image 30 dùng chung · doc feedback 20; window 60s, bật/tắt và window đều config được. Trả <b>429 thật</b> + <code>Retry-After</code> (làm tròn <em>lên</em>) + body <code>JsonResult</code> — đã đọc <code>web/lib/api/client.ts:54-66</code> để xác nhận frontend parse body <em>trước</em> rồi mới check <code>response.ok</code>, nên message đã dịch vẫn hiện, và <code>im.ts</code> dùng chung <code>request</code> đó. <b>Cố ý KHÔNG limit</b> <code>/api/third/*</code> (platform nhận 429 sẽ ngừng retry rồi <em>tắt luôn webhook</em> = mất cả kênh), <code>/api/ws/*</code>, <code>/api/webhooks/*</code> (đã HMAC), <code>/api/dashboard/*</code> (đã auth, staff chung IP sẽ khoá lẫn nhau) — và có test bắn <b>1000 request vào 5 route đó</b>, fail nếu ra bất kỳ 429 nào. 15 test mới, gồm test 16 goroutine × 3200 call đòi đúng 1000 được phép. <b>Giới hạn:</b> counter per-process nên chạy nhiều replica thì bound yếu đi theo số replica; và <b>không chạy được <code>-race</code></b> vì repo build với CGO tắt.</td><td class="loc">toàn bộ internal/</td><td class="v v-y">✓</td></tr>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a minor language inconsistency in this line. The English word 'with' is used in 'vì repo build with CGO tắt', whereas on line 612, the Vietnamese equivalent 'với' is used ('vì repo build với CGO tắt'). Consider changing 'with' to 'với' to maintain consistency in the Vietnamese documentation.

<tr><td class="id">SEC-08 <span class="badge b-good">Done</span></td><td><span class="badge b-med">Med</span></td><td>Không có rate limiting ở bất kỳ đâu (grep 0 kết quả). Khuếch đại SEC-01 (flood 20MB), SEC-09, spam register, và DoS tốn tiền LLM qua flood tin nhắn.<br><b>Đã đóng (PR #14, 2026-09-13).</b> Package <code>internal/pkg/ratelimit</code> fixed-window in-process + middleware, gắn vào 6 route public: login 20 · register 10 · session_exchange <b>120</b> (phải chịu được cả văn phòng sau một NAT) · upload attachment+image 30 dùng chung · doc feedback 20; window 60s, bật/tắt và window đều config được. Trả <b>429 thật</b> + <code>Retry-After</code> (làm tròn <em>lên</em>) + body <code>JsonResult</code> — đã đọc <code>web/lib/api/client.ts:54-66</code> để xác nhận frontend parse body <em>trước</em> rồi mới check <code>response.ok</code>, nên message đã dịch vẫn hiện, và <code>im.ts</code> dùng chung <code>request</code> đó. <b>Cố ý KHÔNG limit</b> <code>/api/third/*</code> (platform nhận 429 sẽ ngừng retry rồi <em>tắt luôn webhook</em> = mất cả kênh), <code>/api/ws/*</code>, <code>/api/webhooks/*</code> (đã HMAC), <code>/api/dashboard/*</code> (đã auth, staff chung IP sẽ khoá lẫn nhau) — và có test bắn <b>1000 request vào 5 route đó</b>, fail nếu ra bất kỳ 429 nào. 15 test mới, gồm test 16 goroutine × 3200 call đòi đúng 1000 được phép. <b>Giới hạn:</b> counter per-process nên chạy nhiều replica thì bound yếu đi theo số replica; và <b>không chạy được <code>-race</code></b> vì repo build với CGO tắt.</td><td class=

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.

1 participant