Skip to content

fix: code-review remediation — /scan/iris quota (H1), signing semaphore (M1), CF-IP (M2) - #8

Draft
Hans1132 wants to merge 5 commits into
mainfrom
claude/code-review-r0c4eq
Draft

fix: code-review remediation — /scan/iris quota (H1), signing semaphore (M1), CF-IP (M2)#8
Hans1132 wants to merge 5 commits into
mainfrom
claude/code-review-r0c4eq

Conversation

@Hans1132

@Hans1132 Hans1132 commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Context

Remediation of three findings from a code review of the payment / signing / input-validation
surfaces. Payment- and crypto-sensitive — opened as draft for Hans's manual review (§6).
Each change is surgical; the full review (with discarded false positives) is in the session.

Changes

H1 — /scan/iris never consumed the daily free quota (fix(scan))

The route ran checkBlacklist + validateSolanaAddress but not the atomic checkFreeQuota
middleware; it read getQuotaStatus() then called consumeFreeQuota(), which is a no-op
outside the middleware. So used never incremented, remaining stayed at the limit, and the
documented 3/IP/day cap never tripped — only the in-memory 10 req/min limiter applied.
→ Route through checkFreeQuota (atomic check+consume, internal calls skipped); delete the
dead inline block.

M1 — signing semaphore double-released on invalid signer output (fix(security))

asyncSign released the semaphore on the success branch, then a JSON.parse failure routed
into fail() which released it again. The unconditional _active-- drifted negative,
raising effective concurrency above SIGN_CONCURRENCY exactly in the degraded state the cap
protects.
settled guard releases the semaphore / settles the promise exactly once, for any
listener (close/error/EPIPE) or parse failure. Regression test asserts the in-flight counter
returns to baseline after a burst of invalid-JSON-but-exit-0 signs.

M2 — req.ip instead of CF-Connecting-IP in /scan/free (fix(scan))

req.ip was used for the internal-A2A CAPTCHA bypass, abuse logging, and quota keying —
violating sharp-edge #1. → _getClientIp(req) at all three sites, matching the rest of the
codebase and removing the XFF-spoof surface.

Verification

  • Full scripts/test-gate.sh: 216 assertions pass, 0 fail across all suites.
  • New regression test in tests/crypto/sign-spof.test.js (3/3); free-quota.test.js 15/15.
  • ⚠️ Gate exits 1 only on the pre-existing npm audit step (a qs DoS CVE in the
    mcp/ subpackage deps) — unrelated to this diff; left untouched (lockfile change needs
    your OK per §5).

Not in scope (flagged for follow-up)

  • The same no-op consumeFreeQuota pattern survives at two more call sites
    (server.js ~1963/1973 and /scan/free 4565/4633) — left for a follow-up to avoid scope creep.
  • Other review findings (SSRF DNS-rebind/redirect, unenforced A2A PDA control, poller
    credit-waste on tx-fetch failure, non-canonical snapshot hash, etc.) remain open.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved free-tier daily quota enforcement across scan endpoints, with atomic check-and-consume behavior and consistent per-IP quota attribution.
    • Updated internal-call detection and CAPTCHA failure handling for the free scan flow to ensure correct quota usage and logging.
    • Fixed a signing concurrency issue where internal semaphores could be released multiple times.
  • Tests
    • Added a regression test to confirm signing cleanup is correct when the signer exits successfully but returns invalid JSON.

claude added 2 commits June 17, 2026 03:39
…er JSON

asyncSign released the concurrency semaphore on the success branch and then,
if JSON.parse threw on malformed sign-report.py output, routed into fail()
which released it a SECOND time. The unconditional _active-- drifted negative,
silently raising effective concurrency above SIGN_CONCURRENCY exactly in the
degraded state the cap exists to protect (CLAUDE.md sharp edge: semaphore must
be released on every exit path — here it was released twice on one path).

Add a settled guard so the semaphore is released and the promise settled
exactly once, regardless of which listener fires (close/error/stdin EPIPE) or
a parse failure. Regression test feeds a python stub that exits 0 with invalid
JSON in a burst and asserts the in-flight counter returns to baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG8kPjhmqwRyjnZAYiYkTb
…/scan/free

H1: /scan/iris ran checkBlacklist+validateSolanaAddress but NOT the atomic
checkFreeQuota middleware; it read getQuotaStatus() then called consumeFreeQuota(),
which is a no-op outside the middleware. So `used` never incremented, `remaining`
stayed at the limit, and the documented 3/IP/day free cap never tripped (only the
in-memory 10 req/min limiter applied). Route the endpoint through checkFreeQuota
(atomic check+consume, internal calls skipped) and drop the dead inline block.

M2: /scan/free used req.ip for the internal-A2A CAPTCHA bypass, abuse logging,
and quota keying, violating sharp edge #1 (trust only CF-Connecting-IP). Swap to
_getClientIp(req) to match the rest of the codebase and remove the XFF spoof
surface.

Same no-op consumeFreeQuota pattern remains at two other call sites
(server.js ~1963/1973 and /scan/free 4565/4633) — flagged for follow-up, out of
this approved scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG8kPjhmqwRyjnZAYiYkTb
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58793872-4ac1-438d-9654-de38d29ceed6

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
📝 Walkthrough

Walkthrough

Three bug fixes: free-quota middleware is enhanced with atomic tryConsumeFreeQuota that fails open on DB errors; /scan/iris moves quota enforcement to checkFreeQuota middleware and removes the no-op inline block; /scan/quick and /scan/free switch to tryConsumeFreeQuota and use _getClientIp(req) for all IP attribution; asyncSign gains a settled guard preventing semaphore double-release on JSON parse errors, with regression test and changelog entry.

Changes

Backend/Security Remediation

Layer / File(s) Summary
Free-quota middleware: tryConsumeFreeQuota helper
src/middleware/free-quota.js, tests/middleware/free-quota.test.js
tryConsumeFreeQuota(ip) performs atomic quota check-and-consume via checkAndConsumeTx, returning denial or success and failing open on DB errors. Module exports the helper; tests verify atomic consumption, correct denial after limit, and shared budget with checkFreeQuota.
Scan routes: quota and IP attribution updates
server.js
/scan/iris adds checkFreeQuota middleware and removes ineffective inline enforcement. /scan/quick free-tier uses tryConsumeFreeQuota with atomic denial. /scan/free replaces req.ip with _getClientIp(req) for CAPTCHA bypass, abuse logging, and quota lookups; quota consumption after cache miss switches to tryConsumeFreeQuota with atomic denial handling. Imports updated for new helpers and _getClientIp alias.
asyncSign semaphore: settled-guard and test export
src/crypto/sign.js
asyncSign introduces settled boolean with fail()/succeed() helpers ensuring semaphore release and promise settlement exactly once, preventing double-release on JSON parse errors. _activeCountForTest() exported to expose in-flight semaphore count.
Semaphore regression test and changelog
tests/crypto/sign-spof.test.js, memory.md
Adds writeStub() helper and regression test spawning a Python stub exiting 0 with invalid JSON; asserts repeated asyncSign calls reject SignPipelineError and verifies _activeCountForTest() baseline unchanged after failures. memory.md records 2026-06-17 remediation and notes remaining follow-up quota issue.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hoppity-hop through the semaphore's gate,
Once and only once — never too late!
The quota now flows where middleware dwells,
And _getClientIp correctly tells.
JSON gone wrong? The guard holds so tight,
All green checkmarks — the warren's alright! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely summarizes the three main fixes (H1: /scan/iris quota, M1: signing semaphore, M2: CF-IP handling) matching the changeset's primary objectives and code review remediation focus.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/code-review-r0c4eq

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

Copy link
Copy Markdown
Owner Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

@Hans1132 Sure, I'll kick off a review of this PR now!

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server.js (1)

4553-4567: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace /scan/free inline quota consumption with the atomic middleware path.

Line 4553 now keys quota by the trusted helper, but this route still uses the same getQuotaStatus() + consumeFreeQuota() pattern that the /scan/iris fix documents as ineffective outside middleware. This leaves /scan/free quota enforcement bypassable even with correct IP attribution. Extract CAPTCHA/type/address validation ahead of the scan handler, then run checkFreeQuota before executing or caching a free scan, using req.freeQuota for response metadata.

Also applies to: 4621-4624

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 4553 - 4567, The /scan/free route currently uses an
inline quota consumption pattern with getQuotaStatus() and consumeFreeQuota()
which is vulnerable to bypass because it's not atomic. Replace this approach by
extracting CAPTCHA, type, and address validation logic ahead of the scan
handler, then apply the atomic checkFreeQuota middleware before the scan is
executed or cached. After the middleware validates quota, use req.freeQuota to
access quota metadata for response construction instead of calling
getQuotaStatus() inline. This ensures quota enforcement is atomic and cannot be
bypassed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@memory.md`:
- Around line 15-20: The 2026-06-17 memory.md entry documenting code-review
remediation (H1/M1/M2) is missing the required backup path field as specified in
the coding guideline. Add a new bullet point under the existing entry that
documents the backup or rollback path (such as a database backup location with
timestamp or a git commit hash for rollback), following the pattern used in
other entries like the 2026-05-21 ones. This is particularly important for
security-sensitive changes affecting payment and signing functionality noted as
§6 requiring Hans's manual review.

In `@server.js`:
- Around line 4463-4465: The CAPTCHA bypass logic in the isInternalA2A variable
uses _getClientIp() which has unsafe fallback behavior, creating a security
vulnerability where any request with the x-a2a-caller header could bypass
CAPTCHA if the CF-Connecting-IP header is missing. Replace the _getClientIp()
authorization check with isInternalCall() helper instead, which properly
validates socket locality rather than relying on header-based IP checks. Keep
_getClientIp() available only for logging and attribution purposes if needed
elsewhere in the code.

---

Outside diff comments:
In `@server.js`:
- Around line 4553-4567: The /scan/free route currently uses an inline quota
consumption pattern with getQuotaStatus() and consumeFreeQuota() which is
vulnerable to bypass because it's not atomic. Replace this approach by
extracting CAPTCHA, type, and address validation logic ahead of the scan
handler, then apply the atomic checkFreeQuota middleware before the scan is
executed or cached. After the middleware validates quota, use req.freeQuota to
access quota metadata for response construction instead of calling
getQuotaStatus() inline. This ensures quota enforcement is atomic and cannot be
bypassed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 989bde8f-17e0-400e-b256-b48710e0b424

📥 Commits

Reviewing files that changed from the base of the PR and between 5fcf592 and ac3f3ec.

📒 Files selected for processing (4)
  • memory.md
  • server.js
  • src/crypto/sign.js
  • tests/crypto/sign-spof.test.js

Comment thread memory.md Outdated
Comment thread server.js
…back fallback

Addresses CodeRabbit review on PR #8. _getClientIp(req) returns the
CF-Connecting-IP header or falls back to 127.0.0.1 when it is absent, so gating
the internal-A2A CAPTCHA bypass on `_getClientIp(req) === '127.0.0.1'` would let
`x-a2a-caller: 1` skip CAPTCHA on any request that reaches Express without the
CF header. Use the canonical isInternalCall(req) predicate (loopback set OR
x-internal-secret) for the bypass; _getClientIp stays for attribution/logging.

Also add the §10 backup/rollback field to the 2026-06-17 memory.md entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG8kPjhmqwRyjnZAYiYkTb

Copy link
Copy Markdown
Owner Author

Re: the outside-diff finding on /scan/free (lines ~4553/4622) — the same no-op consumeFreeQuota quota bypass, also present at ~line 1950.

Confirmed valid. The maintainer has decided to defer this to a follow-up rather than expand this PR's scope. Unlike /scan/iris, /scan/free must consume quota only after CAPTCHA + type-validation + cache-miss (it deliberately consumes on cache-miss only), so it needs an atomic in-handler consume helper rather than the route middleware — a larger change reserved for its own PR.

Tracked in this PR's description ("Not in scope") and in memory.md. Leaving the thread open intentionally as a visible reminder.


Generated by Claude Code

Addresses CodeRabbit CR-3 on PR #8. /scan/quick and /scan/free read quota via
getQuotaStatus() then called consumeFreeQuota(), which is a no-op outside the
checkFreeQuota middleware — so neither route ever incremented the per-IP/global
counters and their shared 3/IP/day free cap was bypassable (same class as the
/scan/iris H1 fix).

Neither route can simply adopt the checkFreeQuota route middleware: /scan/quick
must skip quota for paid (API-key / verified x402) callers, and /scan/free must
consume only after CAPTCHA + a cache miss (cache hits stay free). So add an
atomic tryConsumeFreeQuota(ip) helper (wraps the existing tested
checkAndConsumeTx transaction, fails open on DB error) and call it inline,
inside each route's existing paid/CAPTCHA/cache gating and guarded by
!isInternalCall(req). The 3/IP/day budget is now shared and enforced across
/scan/iris, /scan/quick, and /scan/free.

consumeFreeQuota stays a no-op for backward compat. Adds tryConsumeFreeQuota
unit tests (atomic increment, denial past limit, shared budget with the
middleware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG8kPjhmqwRyjnZAYiYkTb

Copy link
Copy Markdown
Owner Author

Update on the /scan/free + /scan/quick quota bypass (CR-3): now fixed in b99abe5, not deferred — superseding my earlier "deferred" note.

Approach: both routes can't use the checkFreeQuota route middleware (/scan/quick must skip quota for paid/API-key/x402 callers; /scan/free must consume only after CAPTCHA + cache-miss). So added an atomic tryConsumeFreeQuota(ip) helper (wraps the existing tested checkAndConsumeTx, fails open on DB error) and call it inline within each route's existing gating, guarded by !isInternalCall(req). The 3/IP/day budget is now shared and enforced across /scan/iris, /scan/quick, and /scan/free. Added tryConsumeFreeQuota unit tests (18/18 in the quota suite).


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

@Hans1132 Sure, I'll kick off a review of the updated PR now, including the tryConsumeFreeQuota fix in b99abe5!


✏️ Learnings added
Learnt from: Hans1132
URL: https://github.com/Hans1132/integrity.molt/pull/8

Timestamp: 2026-06-23T09:06:24.926Z
Learning: In the `integrity.molt` project (Hans1132/integrity.molt), the `/scan/free` and `/scan/quick` endpoints cannot use the `checkFreeQuota` route middleware for quota enforcement because: `/scan/quick` must skip quota for paid/API-key/x402 callers, and `/scan/free` must consume quota only after CAPTCHA + cache-miss. Instead, an atomic inline helper `tryConsumeFreeQuota(ip)` (wrapping the existing tested `checkAndConsumeTx`, failing open on DB error) is called within each route's gating logic, guarded by `!isInternalCall(req)`. The 3/IP/day free quota budget is shared across `/scan/iris`, `/scan/quick`, and `/scan/free`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/middleware/free-quota.test.js (1)

221-255: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add branch coverage for tryConsumeFreeQuota global and DB-error paths.

The new tests validate IP-limit behavior, but they miss two new helper branches: denied: 'global' and fail-open { ok: true, dbError: true }. Adding those two cases will lock in the intended behavior and prevent silent regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/middleware/free-quota.test.js` around lines 221 - 255, The test suite
for tryConsumeFreeQuota is missing coverage for two important branches: the
global quota exhaustion case that should return denied with value 'global', and
the database error fail-open path that should return ok: true with dbError:
true. Add two new test cases after the existing tryConsumeFreeQuota tests: one
that exhausts the global quota limit (PER_GLOBAL_DAILY_LIMIT) and verifies the
denied: 'global' response is returned, and another that simulates a database
error condition and verifies that the function fails open by returning ok: true
with dbError: true to allow the request through while logging the error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server.js`:
- Around line 1953-1959: The tryConsumeFreeQuota function can return denied as
either 'global' or a per-IP denial, but the current code at the quota.denied
check treats all denials as per-IP denials and always responds with per-IP quota
messages and data. You need to separate the handling of global denials from
per-IP denials: add a specific check for quota.denied === 'global' to handle
global cap denials separately with different response data (using globalUsed and
global_limit fields), and keep the existing per-IP quota response for non-global
denials. Apply this same fix in both locations where this pattern appears
(around line 1954 and the other location mentioned at lines 4627-4634).

---

Nitpick comments:
In `@tests/middleware/free-quota.test.js`:
- Around line 221-255: The test suite for tryConsumeFreeQuota is missing
coverage for two important branches: the global quota exhaustion case that
should return denied with value 'global', and the database error fail-open path
that should return ok: true with dbError: true. Add two new test cases after the
existing tryConsumeFreeQuota tests: one that exhausts the global quota limit
(PER_GLOBAL_DAILY_LIMIT) and verifies the denied: 'global' response is returned,
and another that simulates a database error condition and verifies that the
function fails open by returning ok: true with dbError: true to allow the
request through while logging the error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 40ff3b16-5932-4e49-8be2-37cf8d28025b

📥 Commits

Reviewing files that changed from the base of the PR and between ac3f3ec and b99abe5.

📒 Files selected for processing (4)
  • memory.md
  • server.js
  • src/middleware/free-quota.js
  • tests/middleware/free-quota.test.js
✅ Files skipped from review due to trivial changes (1)
  • memory.md

Comment thread server.js

Copy link
Copy Markdown
Owner Author

@coderabbitai full review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

@Hans1132 Sure, I'll redo a full review of the PR now, covering all the changes across the quota enforcement, semaphore fix, IP detection, and the new tryConsumeFreeQuota helper!

✅ Action performed

Full review finished.

…responses

Addresses CodeRabbit review on PR #8. tryConsumeFreeQuota() can deny with either
'ip' (per-IP daily cap) or 'global' (global daily cap), but the inline handlers
on /scan/quick and /scan/free treated every denial as per-IP — mislabeling a
global-cap denial and dropping the global_used/global_limit fields. Branch on
denied === 'global' first (matching the global-cap response shape already used
elsewhere in /scan/free), then fall through to the per-IP response.

Adds free-quota tests for the denied:'global' branch and the DB-error fail-open
path ({ ok: true, dbError: true }).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG8kPjhmqwRyjnZAYiYkTb
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.

2 participants