Skip to content

security: re-audit P0/P1 remediations + E2E follow-up (builds on #44-#48)#49

Open
xcantera wants to merge 4 commits intoXinFinOrg:masterfrom
xcantera:security-fixes-2026-04-24
Open

security: re-audit P0/P1 remediations + E2E follow-up (builds on #44-#48)#49
xcantera wants to merge 4 commits intoXinFinOrg:masterfrom
xcantera:security-fixes-2026-04-24

Conversation

@xcantera
Copy link
Copy Markdown

@xcantera xcantera commented Apr 27, 2026

Context

This PR is the result of an external security re-audit of master.xinfin.network / MasterNode-App performed 2026-04-24, followed by an end-to-end local reproduction pass on 2026-04-27.

It builds strictly on top of the security work already merged in #44, #45, #46, #47, #48. Every helper, allowlist, signer-recovery shim, fallback header, CSP directive, and CORS handler that those PRs introduced is preserved — this PR adds another layer rather than rewriting them. Where one of those layers had a residual issue we found during reproduction, the change is described explicitly below.

We're submitting this publicly because the fork has been published while the audit was running, the underlying fixes (login QR replay, KYC file substitution, CSP downgrade, MongoDB without auth, etc.) overlap with the recently merged hardening work, and there is no SECURITY.md / private security advisory channel set up on the repo. If you'd prefer the rest of the audit notes (SECURITY_FIXES.md, LIVE_EXPOSURE_REPORT.md) handled privately, happy to coordinate offline before merge — let me know.

What's preserved from upstream

File Upstream PR Marker we kept
index.js #44 frameguard: deny, referrerPolicy: strict-origin-when-cross-origin, hidePoweredBy, base CSP
index.js #46 Loosened connectSrc / imgSrc for runtime needs
index.js #47 Single-call helmet({...}) structure, Google Analytics origins (scriptSrc / imgSrc / connectSrc)
index.js #48 Function-based CORS origin check + 403 error handler with explanatory body
apis/candidates.js #44 ALLOWED_SCHEMES URL allowlist, validateUrl(...), web3.eth.accounts.recover owner check, signed-message + nonce update flow
apis/candidates.js #48 Default-empty rewards structure (XDCscan call commented out)
apis/voters.js #44 ALLOWED_SORT_FIELDS + normalizeSortField
apis/ipfs.js #44 toHexAddress, normalizeValue, unauthorized, multi-format signer recovery (plain / utf8ToHex), `x-kyc-account
app/components/candidates/Apply.vue #44 signerAccount xdc→0x normalisation, personal.sign with eth.sign fallback, x-kyc-* headers

What's new in this PR

Re-audit (P0 / P1) — already merged on this branch in earlier commits

  • H-2 — Login QR session binding: server now mints a per-QR uuid in the message itself (apis/auth.js generateLoginQR); SPA passes it through to /verifyLogin; Setting.vue switched to encodeURIComponent because the message now contains = (would otherwise be split into the wrong query keys by mobile wallets).
  • H-7 — EIP-155 chainId binding on /api/voters/verifyTx: parses the legacy raw tx v byte and rejects unprotected (v in {27,28}) or wrong-chain transactions.
  • M-3 — Stack trace / internal path leakage: middlewares/error.js returns generic envelopes in production, full stack only behind IS_DEV_ENV.
  • M-4 — Verbose request logging: PR added logs and updated cors and removed redunct api call #48's console.log per request gated behind DEBUG_REQUESTS and routed through winston so client IPs / origins / referers don't hit prod stdout.
  • M-6 — Unauthenticated Swagger UI: /api-docs requires HTTP Basic Auth from SWAGGER_USER / SWAGGER_PASS in production; disabled outright if either is unset.
  • M-7 — express-fileupload hardening: useTempFiles, hard 10 MiB fileSize, files: 1, abortOnLimit, safeFileNames, preserveExtension: 4.
  • M-9 — KYC file-substitution attack (closes a residual issue in Fixed the issue of using static api key for ipfs upload #45): two-step flow with a server-issued single-use nonce + sha256 of the file embedded in the signed message:
    1. POST /api/ipfs/requestKYCNonce — mint a uuid bound to account, TTL 5 min.
    2. Client signs [XDCmaster KYC <nonce>] Upload <fileHash> for <account>.
    3. POST /api/ipfs/addKYC — server atomically consumes the nonce, recomputes sha256(file), reconstructs the message, recovers the signer (still using Fixed the security issues #44's multi-format fallback), and rejects on any mismatch.
  • Rate limiting: per-IP buckets in helpers/rateLimiters.js applied to login, signature, and write endpoints (uses app.set('trust proxy', ...) from env so we don't bucket every real client into the proxy IP).
  • CSP: production script-src drops 'unsafe-eval'; HSTS preload + upgrade-insecure-requests only when IS_PRODUCTION. Dev still ships 'unsafe-eval' + ws: so webpack-dev-server HMR works.

E2E follow-up pass (this commit, 61fad27)

  • F-1 — Dockerfile: npm install --legacy-peer-deps --ignore-scripts in both stages (skips the sha3 native compile that fails on Node 20); replace the final chown -R masternode:masternode /app with COPY --chown=masternode:masternode per stage + a tiny chown for tmp/sslcert. Was hanging for several minutes on overlay/WSL2 filesystems because of the recursive chown over node_modules.
  • F-2 — app/components/Setting.vue: encodeURIComponent for both message and submitURL (see H-2 above).
  • F-3 — app/components/candidates/Apply.vue: uploadKYC() rewired to the two-step flow described in M-9.
  • F-4 — Lateral-takeover fix on /verifyLogin: discovered during the live E2E pass. The previous lookup was findOneAndUpdate({ signedAddress }), so a second wallet that scanned the victim's QR with their own client could overwrite the same login session and hijack the SPA polling /getLoginResult. Fix:
    • models/mongodb/signature.js: signedId carries unique: true, sparse: true (atomic guarantee under concurrent inserts).
    • apis/auth.js verifyLogin: rejects re-claim by a different signer; treats a same-signer retry as idempotent success.
  • F-5 — Production blank-page fix in app/app.js: switch the root Vue instance from template: '<App/>' to render: h => h(App). Webpack 5 + terser was tree-shaking the Vue 2.7 runtime template compiler in the production build, leaving the root instance unable to resolve <App/> and rendering an empty comment node.
  • F-6 — package.json: move connect-flash from devDependencies to dependencies. It's required at runtime by index.js (app.use(flash())) and the production Docker image was crashing on first request without it.

Deliberate departure from #44

PR #44 added an x-api-key shared-secret bypass on /addKYC for trusted server-to-server uploads. We dropped that bypass: a single shared key short-circuits all the per-upload signature checks, and a static secret stored in config + travelling in plaintext over a header is a strictly worse attack surface than what the per-request flow defends against. Happy to add it back behind helmet-style strict opt-in (e.g. only mounted when config.security.ipfsUploadApiKey is set and process.env.NODE_ENV === 'development') if there's a deployment that needs it — please let us know the use case and we'll wire that up in a follow-up commit.

Files in this PR

apis/auth.js
apis/candidates.js
apis/index.js
apis/ipfs.js
apis/voters.js
app/app.js
app/components/Setting.vue
app/components/candidates/Apply.vue
build/index.html, build/<new bundle hashes>
helpers/rateLimiters.js                (new)
index.js
middlewares/error.js
models/mongodb/index.js
models/mongodb/ipfsNonce.js            (new)
models/mongodb/signature.js
package.json
sslcert/README.md, sslcert/server.{crt,key}, travis.pem.enc   (removed; pre-committed test secrets)
.env.example, .gitignore, Dockerfile
SECURITY_FIXES.md, LIVE_EXPOSURE_REPORT.md   (audit notes)
test-harness/soak.sh                   (parametrised soak harness)

Pre-deploy migration note for ops

The Signature collection's existing signedId_1 index is non-unique. After deploy, drop it and let Mongoose recreate the new unique+sparse one. If you can't take a brief lock window, this works:

// 1. Backfill: collapse duplicate signedIds (keep the most recent claim)
db.signatures.aggregate([
  { $sort: { updatedAt: -1 } },
  { $group: { _id: '$signedId', keep: { $first: '$_id' } } }
]).forEach(g => {
  db.signatures.deleteMany({ signedId: g._id, _id: { $ne: g.keep } })
})

// 2. Rebuild index
db.signatures.dropIndex('signedId_1')
db.signatures.createIndex({ signedId: 1 }, { unique: true, sparse: true, background: true })

Verification performed locally

  • npm install --legacy-peer-deps --ignore-scripts (clean)
  • ESLint on every changed JS / Vue file (0 errors)
  • vue-template-compiler on Apply.vue + Setting.vue (0 errors)
  • Webpack production build (devtool: false, no .map files emitted)
  • Headless-Chromium walk of the SPA: login QR + KYC + voting flows
  • E2E suite for /api/auth/* and /api/ipfs/* with real EIP-191 signatures (incl. nonce replay, file-hash mismatch, lateral takeover)
  • E2E suite for /api/voters/verifyTx with real EIP-155 signed legacy txs (correct chainId, wrong chainId, unprotected v∈{27,28})
  • docker build + docker run against local Mongo, smoke-hit /api/config and a rate-limited endpoint
  • 5-minute soak (8 workers, mixed read endpoints): stable memory, no leaks, rate-limit kicks in correctly
  • npm test (Truffle): pre-existing Node 20 incompatibility (TypeError: Cannot read properties of undefined (reading 'type')) — not introduced by this PR; no contracts/, test/, or truffle-config.js files modified.

Out of scope

  • npm audit reports 120 transitive vulnerabilities (42 critical, 38 high), almost all from @walletconnect/*, truffle-hdwallet-provider, elliptic, axios, web3, ethers. Bumping these majors would break the wallet integration contract surface — tracked under "known remaining work" in SECURITY_FIXES.md. Live-exposure analysis (LIVE_EXPOSURE_REPORT.md) shows none are exploitable at the masterchain.xinfin.network surface area, but the dependency upgrade should still be tackled as its own piece of work.

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented rate limiting across all API endpoints to protect against abuse.
    • Added two-step KYC upload process with nonce-based signatures for enhanced security.
  • Bug Fixes

    • Hardened QR code login with UUID validation and expiration checks to prevent replay attacks.
    • Fixed transaction chain validation to reject mismatched network signatures.
    • Improved error message sanitization in production to prevent information leakage.
  • Security Enhancements

    • Enabled Swagger UI authentication in production environments.
    • Improved file upload validation with size limits and secure handling.
    • Enhanced CSP headers and added HSTS protection for production.
    • Removed SSL certificates from version control; updated Docker security posture.
  • Documentation

    • Added environment configuration template and security audit documentation.

Security Audit Bot added 3 commits April 24, 2026 20:14
Addresses the critical and high-severity findings documented in the April 2026
security audit. Full per-file breakdown is in SECURITY_FIXES.md.

Highlights:
  - IPFS KYC upload rewritten with server-issued nonce + file-hash binding
    so a captured signature can no longer be reused on any other file (M-9).
  - Hardened CSP: removed 'unsafe-eval' and http:// sources in production,
    added HSTS preload and upgrade-insecure-requests.
  - express-rate-limit wired into auth / tx / search / upload / mutation
    routers via helpers/rateLimiters.js (M-2).
  - /verifyTx now parses the raw tx, extracts the EIP-155 chainId from v,
    and rejects unprotected or cross-chain transactions (H-7).
  - /candidates/search escapes regex metacharacters via lodash.escaperegexp
    and caps query length to defeat ReDoS (H-1); /listByHash enforces CSV
    string format and caps list length.
  - All escape() calls replaced with strict UUID-v4 validation; every
    console.trace(e) stack-trace leak removed.
  - Error middleware rewritten to sanitize client-facing responses while
    preserving full-fidelity server logs (M-4).
  - MongoDB connection now reads MONGO_URI/DB_URI env vars so authenticated
    URIs stay out of config files; /api-docs gated behind basic auth in
    production.
  - Committed secrets removed: sslcert/server.key, sslcert/server.crt,
    travis.pem.enc. .gitignore expanded to block re-introduction.
  - Dockerfile upgraded from EOL node:16.16.0 to node:20.18.0 with a
    multi-stage build and non-root runtime user.
  - xdc3 pinned from "latest" to 1.3.13416; lodash bumped to 4.17.21;
    express-fileupload moved to runtime deps.

Breaking change: the old /api/ipfs/addKYC body schema is no longer accepted.
Clients must call /api/ipfs/requestKYCNonce first, sign the returned
message (which includes sha256(file) at submit time), and pass the nonce
alongside the signed message on upload. See SECURITY_FIXES.md for details.

Operational follow-ups still required (out-of-band):
  - Rotate TLS certificate issued against the removed server.key
  - Rotate any credentials protected by travis.pem.enc
  - Enable MongoDB authentication and set MONGO_URI in production env

Made-with: Cursor
… CVEs

Reachability analysis of the 120 npm audit findings against the production
Node server. Categorizes advisories into server-runtime (61),
browser-bundle-only (24), and dev/build-only (14); traces every entry to
its top-level requirer; intersects with the set of packages the server
actually require()s.

Non-destructive HTTP probes against master.xinfin.network confirm the
live site is still running the pre-fix upstream (signed QR message has
no id binding, /api/ipfs/requestKYCNonce returns 404, Swagger UI at
/api-docs/ is ungated, 60 consecutive /api/auth/generateLoginQR calls
return 200 with no ratelimit-* headers, raw library errors leak).

Of the 61 server-runtime CVEs, only two (elliptic signature malleability
and qs query-string DoS) have a plausible live-exploit path, and both
are mitigated by the fixes in this branch. The real live risk today is
the 8 P0/P1 application-level findings from the re-audit, all of which
this branch's commit 3731be5 closes.

Made-with: Cursor
XinFinOrg#44-XinFinOrg#48

Builds on top of upstream XinFinOrg#44, XinFinOrg#45, XinFinOrg#46, XinFinOrg#47, XinFinOrg#48 (already merged into master).
Every helper they added (toHexAddress, normalizeValue, unauthorized, multi-format
signer recovery, x-kyc-* header fallbacks, ALLOWED_SCHEMES URL allowlist, sort-
field allowlist, single-helmet structure, function-based CORS + error handler,
default-rewards structure) is preserved.

Fixes found during the local end-to-end pass:

F-1 Dockerfile build:
    - npm install --legacy-peer-deps --ignore-scripts in both stages: skips
      sha3 native compilation that fails on Node 20.
    - Replace final chown -R masternode:masternode /app with COPY --chown
      on each stage and a tiny chown for tmp/sslcert. The recursive chown
      over node_modules was hanging on overlay/WSL2 filesystems.

F-2 app/components/Setting.vue (XSS-class adjacent / wallet UX):
    encodeURIComponent (not encodeURI) for both message and submitURL when
    constructing xdcchain://login URIs. The server-issued message now
    contains "id=<uuid>" (audit fix H-2 / login-QR session binding) and
    encodeURI does not escape "=", so the mobile wallet URL parser was
    splitting that token into the wrong query keys.

F-3 app/components/candidates/Apply.vue (KYC client):
    Rewire uploadKYC() to the two-step flow:
      1. Compute sha256(file) in the browser via window.crypto.subtle.
      2. POST /api/ipfs/requestKYCNonce to mint a one-time nonce.
      3. Sign exactly "[XDCmaster KYC <nonce>] Upload <fileHash> for <acct>"
         and submit signature + nonce + file to /api/ipfs/addKYC.
    Replaces the timestamp-only message that PR XinFinOrg#44 wired (which still
    allowed any captured signed message to be paired with any other file
    within the 5-minute window). x-kyc-* header set is preserved.

F-4 apis/auth.js + models/mongodb/signature.js (lateral-takeover fix):
    Single-use binding of signedId -> signedAddress. The previous lookup
    was by signedAddress alone, which let a second wallet that scanned
    the victim's QR with their own client overwrite the same login
    session and hijack the SPA polling /getLoginResult. Now:
      - signedId carries a unique+sparse index (atomic guarantee under
        concurrent inserts).
      - verifyLogin rejects re-claim by a different signer and treats a
        same-signer retry as idempotent success.
      - 24h TTL on the row is refreshed on every successful login.
    Ops note: existing deployments must drop the old non-unique index
    before bouncing — see SECURITY_FIXES.md "Pre-deploy migration note".

F-5 app/app.js (production blank-page fix):
    Switch the root Vue instance from template:'<App/>' to
    render: h => h(App). Webpack 5 + terser was tree-shaking the Vue
    2.7 runtime template compiler in the production build, leaving the
    root instance unable to resolve <App/> and rendering an empty
    comment node. Render functions are statically analysable and never
    DCE'd.

F-6 package.json (runtime dependency):
    Move connect-flash from devDependencies to dependencies. It is
    required at runtime by index.js (app.use(flash())) and the Docker
    image was crashing on first request without it.

Also includes:
- SECURITY_FIXES.md: appended "Follow-up E2E pass" section (F-1..F-6),
  the Signature.signedId index migration note for ops, and the rationale
  for not adopting PR XinFinOrg#44's optional x-api-key bypass on /addKYC.
- test-harness/soak.sh: parametrised 5-minute soak harness used during
  the local pass (no secrets, all knobs env-overridable).
- Webpack production bundles regenerated to pick up F-3 and F-5; old
  fingerprinted bundles removed accordingly.

Verification performed locally:
- npm install --legacy-peer-deps --ignore-scripts (clean)
- ESLint on every changed JS / Vue file (0 errors)
- vue-template-compiler on Apply.vue + Setting.vue (0 errors)
- Webpack production build (devtool:false, no source maps emitted)
- Headless-Chromium walk of the SPA: login QR + KYC + voting flows
- E2E suite for /api/auth/* and /api/ipfs/* with real EIP-191 signatures
- E2E suite for /api/voters/verifyTx with EIP-155 chainId binding
- docker build + docker run against local Mongo, smoke-hit /api/config
- 5-minute soak (8 workers, mixed read endpoints): stable mem, no leaks
- npm test (Truffle): pre-existing Node 20 incompatibility, unchanged

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@xcantera has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 40 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c2a4f15-4f89-4484-b2ba-23cf34662fee

📥 Commits

Reviewing files that changed from the base of the PR and between 61fad27 and f624e7c.

📒 Files selected for processing (19)
  • .env.example
  • Dockerfile
  • LIVE_EXPOSURE_REPORT.md
  • SECURITY_FIXES.md
  • apis/auth.js
  • apis/candidates.js
  • apis/ipfs.js
  • apis/voters.js
  • app/components/candidates/Apply.vue
  • build/app.51a7fb9d79b73238fbbd.js
  • build/index.html
  • helpers/rateLimiters.js
  • index.js
  • middlewares/error.js
  • models/mongodb/index.js
  • models/mongodb/ipfsNonce.js
  • models/mongodb/signature.js
  • sslcert/README.md
  • test-harness/soak.sh
📝 Walkthrough

Walkthrough

This comprehensive security-hardening PR introduces environment-based configuration, multi-stage Docker builds with Node 20 and a non-root user, refactors QR login and KYC upload workflows to prevent replay attacks via UUID and nonce binding, centralizes rate limiting across APIs, hardens file uploads and headers, removes TLS keys from version control, and sanitizes error messages in production.

Changes

Cohort / File(s) Summary
Configuration & Environment
.env.example, .gitignore
New .env.example template documenting required/optional environment variables; .gitignore now excludes TLS/keystore files (\*.key, \*.crt, \*.p12, \*.pfx), environment files (except .env.example), and sensitive paths.
Infrastructure & Deployment
Dockerfile, sslcert/..., package.json, index.js
Dockerfile migrated to multi-stage Node 20 build with non-root masternode user, temp file uploads, and direct node index.js execution; TLS certificate/key removed from sslcert/; dependencies updated (lodash, xdc3) and production dependencies added (express-basic-auth, express-rate-limit, connect-flash moved); index.js adds reverse-proxy trust, CSP/HSTS/body-size hardening, Swagger basic-auth gating in production, request logging guards, and CORS response reduction.
API Security Hardening
apis/auth.js, apis/ipfs.js, apis/candidates.js, apis/voters.js, apis/index.js
QR login tightened with UUID + ISO timestamp embedding and signedId binding to prevent replay; KYC refactored from timestamp-window to two-step nonce-bound protocol (/requestKYCNonce + /addKYC); candidate search/verification endpoints now validate UUIDs and bound input lengths; transaction verification adds EIP-155 chain-binding; centralized rate limiting applied via new rateLimiters module across read/auth/transaction/search/upload endpoints.
Middleware & Database
middlewares/error.js, models/mongodb/index.js, models/mongodb/ipfsNonce.js, models/mongodb/signature.js
Error handler refactored to sanitize production error messages (removing paths/stack frames) and use structured logging; MongoDB connection masks credentials in logs and adds timeout/topology config; new IpfsNonce model with 300-second TTL for one-use nonces; Signature model's signedId field changed to unique: true, sparse: true.
Frontend & Build
app/app.js, app/components/..., build/index.html, build/runtime.*, build/vendor.*, build/node-vendor.*
Root Vue component switched to explicit render function; QR-link generation updated to use encodeURIComponent for proper = escaping; KYC uploader refactored to request nonce and compute file SHA-256 hash before signing; build bundles replaced with new hashes; runtime/vendor chunks reorganized with new webpack configuration.
Documentation & Testing
LIVE_EXPOSURE_REPORT.md, SECURITY_FIXES.md, sslcert/README.md, test-harness/soak.sh
Two new reports documenting exposure findings, applied fixes, deployment checklist, and known remaining work; sslcert/README.md instructs local HTTPS setup after key removal; new soak-test harness for load/resource metrics validation.
Rate Limiting Module
helpers/rateLimiters.js
Introduces centralized Express rate-limiting with six endpoint-specific limiters (auth, transaction, search, upload, read, mutation) using per-route windowMs and max configurations and consistent 429 error handler.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Browser
    participant Server as Server
    participant DB as MongoDB
    participant Blockchain as Blockchain

    rect rgba(100, 200, 150, 0.5)
    Note over User,Blockchain: QR Login Flow with Replay Prevention
    User->>Client: Request login QR
    Client->>Server: GET /api/auth/generateLoginQR
    Server->>DB: Generate UUID, create record
    Server-->>Client: Return QR with message "Login id=<UUID> [XDCmaster <timestamp>]"
    Client->>User: Display QR code
    
    User->>Client: Scan QR in wallet
    Client->>Blockchain: Sign message with private key
    Client->>Server: POST /api/auth/verifyLogin {id, message, signature, signer}
    
    Server->>Server: Validate UUID v4 format
    Server->>Server: Extract & validate timestamp from message
    Server->>Server: Reject if timestamp beyond LOGIN_SIGNATURE_TTL_MS
    Server->>Server: Verify signature matches message/signer
    Server->>DB: Query existing QR by signedId (idempotent check)
    alt QR already used by same signer
        Server-->>Client: Return success (idempotent retry)
    else First use or different signer
        Server->>DB: Upsert QR with signedId, reject if duplicate
        Server-->>Client: Return success
    end
    end
Loading
sequenceDiagram
    actor User
    participant Client as Browser
    participant Server as Server
    participant DB as MongoDB
    participant IPFS as IPFS
    participant Signer as Wallet

    rect rgba(100, 150, 200, 0.5)
    Note over User,Signer: Two-Step KYC Nonce-Bound Protocol
    User->>Client: Select KYC PDF file
    Client->>Client: Compute SHA-256 file hash
    Client->>Server: POST /api/ipfs/requestKYCNonce {account}
    Server->>DB: Generate UUID nonce, store as unconsumed
    Server-->>Client: Return {nonce, message template with nonce/TTL, fileHashInstructions}
    
    Client->>Signer: Request signature of {nonce, fileHash, account}
    Signer-->>Client: Return signed message
    
    Client->>Server: POST /api/ipfs/addKYC {account, nonce, signedMessage, file}
    Server->>Server: Validate nonce is UUIDv4
    Server->>Server: Compute SHA-256 of uploaded bytes
    Server->>Server: Reconstruct expected message from {nonce, fileHash, account}
    Server->>Server: Recover signer from signature, verify matches account
    Server->>DB: findOneAndUpdate nonce to consumed (atomic, TTL check)
    alt Nonce expired or already consumed
        Server-->>Client: Reject (reuse prevented)
    else Valid & unconsumed
        Server->>IPFS: Upload verified file bytes
        Server-->>Client: Return {ipfsHash, fileHash}
    end
    end
Loading
sequenceDiagram
    participant Client as Client
    participant ReadLimiter as Read Limiter
    participant AuthLimiter as Auth Limiter
    participant Router as Route Handler
    participant RateLimitStore as Store

    rect rgba(200, 150, 100, 0.5)
    Note over Client,RateLimitStore: Centralized Rate Limiting Stack
    Client->>ReadLimiter: Request to /api/voters/getList
    ReadLimiter->>RateLimitStore: Check request count in window
    alt Requests within limit
        RateLimitStore-->>ReadLimiter: Allow
        ReadLimiter->>Router: Pass through
        Router-->>Client: 200 OK
    else Limit exceeded
        RateLimitStore-->>ReadLimiter: Reject
        ReadLimiter-->>Client: 429 Too many requests
    end
    
    Client->>AuthLimiter: Request to /api/auth/verifyLogin
    AuthLimiter->>RateLimitStore: Check stricter auth window/max
    alt Within stricter limits
        AuthLimiter->>Router: Pass through
        Router-->>Client: 200/401/other
    else Limit exceeded
        AuthLimiter-->>Client: 429 Too many requests
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Fixed file upload issue #41: Modifies apis/ipfs.js KYC upload handler to add validation and file size limits (10MB), directly complementing this PR's two-step nonce-bound refactoring of the same endpoint.

Poem

🐰 The burrow grows safer, one hoppy commit!
QR codes bound to time, nonces that don't repeat—
No replay tricks, no key exposure beneath,
Rate limits stand guard, CSP headers gleam,
Secure by design, this rabbit approves the dream! 🔐✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the primary focus: security remediations and end-to-end follow-up work from an audit. It is specific, concise, and accurately reflects the main changes across API hardening, Docker, Vue, and configuration updates.
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

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (9)
build/index.html (1)

1-1: Build artifact — committing build/ keeps webpack output under version control.

Just noting that build/index.html and the hashed bundles are regenerated on every production build, so every PR that touches frontend code will produce noisy diffs and merge conflicts here. If the deploy pipeline already runs webpack --mode=production, consider adding /build/ to .gitignore and shipping these artifacts only via CI/CD. Out of scope for this PR — flagging only because .gitignore is also being updated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build/index.html` at line 1, The PR accidentally commits generated frontend
artifacts (build/index.html and the hashed bundles like runtime.*.js, app.*.js,
vendor.*.js) which should not be versioned; remove the entire build/ directory
from the repo, add build/ to .gitignore, and update CI/CD to produce and publish
these artifacts during the deployment step (so consumers still get runtime.*,
node-vendor.*, app.*, style.* and vendor.* bundles) to avoid noisy diffs and
merge conflicts.
sslcert/README.md (1)

11-14: Add a language identifier to the fenced code block.

markdownlint MD040 — also gives nicer rendering on GitHub.

-```
-openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt \
-  -days 365 -subj "/CN=localhost"
-```
+```bash
+openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt \
+  -days 365 -subj "/CN=localhost"
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sslcert/README.md` around lines 11 - 14, Update the fenced code block in
README.md that contains the openssl command to include a language identifier;
replace the opening triple backticks with ```bash so the block starting with
"openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt \
..." is changed to a bash fenced block for proper markdownlint (MD040) and
GitHub rendering.
helpers/rateLimiters.js (1)

34-38: Dead message field on authLimiter.

Because buildLimiter installs a custom handler that always responds with { status: 429, error: { message: 'Too many requests, please retry later.' } }, the message: 'Too many authentication attempts.' here is never sent. Either remove it for consistency with the other limiters, or fold it into the handler if you actually want a different copy on this surface.

♻️ Suggested cleanup
 const authLimiter = buildLimiter({
     windowMs: WINDOW_15_MIN,
-    max: 60,
-    message: 'Too many authentication attempts.'
+    max: 60
 })

Or, if a distinct message is desired, override the handler per-limiter:

-const authLimiter = buildLimiter({
-    windowMs: WINDOW_15_MIN,
-    max: 60,
-    message: 'Too many authentication attempts.'
-})
+const authLimiter = buildLimiter({
+    windowMs: WINDOW_15_MIN,
+    max: 60,
+    handler: (req, res) => res.status(429).json({
+        status: 429,
+        error: { message: 'Too many authentication attempts.' }
+    })
+})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@helpers/rateLimiters.js` around lines 34 - 38, The authLimiter config sets
message: 'Too many authentication attempts.' which is never used because
buildLimiter installs a global custom handler that always returns the standard
429 payload; update authLimiter by either removing the dead message property for
consistency with other limiters, or if you need a custom auth-specific message,
modify authLimiter to supply a per-limiter handler that returns your custom
payload (referencing authLimiter and buildLimiter/handler to locate the code) so
the auth message is actually sent.
Dockerfile (1)

41-48: Comment and implementation disagree: chown -R node_modules is exactly what the comment claims to avoid.

The block comment on lines 41–44 says you’re using COPY --chown=... to skip the slow recursive chown over node_modules on overlay FS, but npm install here runs as root (no USER switch yet), so node_modules ends up root-owned and line 48 then performs chown -R masternode:masternode node_modules — the very operation the comment says is being avoided. On a multi-million-file node_modules this is the slow path the comment promises you don’t pay.

Two cheap ways to actually realize the optimization:

♻️ Option A — install as the unprivileged user (preferred)
-COPY --from=build --chown=masternode:masternode /app/package*.json ./
-RUN npm install --omit=dev --legacy-peer-deps --ignore-scripts \
-    && npm cache clean --force \
-    && chown -R masternode:masternode node_modules
+COPY --from=build --chown=masternode:masternode /app/package*.json ./
+USER masternode
+RUN npm install --omit=dev --legacy-peer-deps --ignore-scripts \
+    && npm cache clean --force

(Move the existing USER masternode directive on line 73 up here, and ensure /app is writable by masternode — which it already is given the mkdir/chown you do for tmp and sslcert.)

♻️ Option B — install in the build stage and copy with --chown

Run npm prune --production (or a separate npm ci --omit=dev) in the build stage and copy the resulting node_modules with --chown=masternode:masternode, eliminating the runtime npm install entirely.

If the goal is just code, not perf, then please update the comment to match what the code actually does so the next person isn’t misled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 41 - 48, The comment claims we avoid a recursive
chown by using COPY --chown, but the current runtime steps run npm install as
root and then run chown -R masternode:masternode node_modules, which negates
that optimization; fix by either (A) moving the USER masternode directive up
before the runtime RUN npm install so npm install runs as masternode (ensure
/app is writable), or (B) perform production install/prune in the build stage
and COPY --from=build the node_modules with --chown=masternode:masternode to the
final image removing the runtime npm install and chown, and if you intentionally
keep the current root-install-then-chown behavior, update the comment to reflect
that fact instead of claiming the chown is avoided (referencing RUN npm install,
chown -R masternode:masternode node_modules, USER masternode, and COPY
--from=build --chown).
apis/candidates.js (1)

938-999: UUID validation is doubled — keep one or note why both are needed.

query('id').isUUID(4) on lines 939 and 989 already enforces strict v4 syntax, so the inner isValidUuid(id) checks on lines 952 and 997 only fire if a previous middleware forgets to short-circuit on validator errors. Not harmful — the redundancy gives belt-and-braces — but you can drop the inner check to keep one source of truth, or keep it and add a brief comment that it’s a defense-in-depth guard against future bypasses of the validator stack.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apis/candidates.js` around lines 938 - 999, The code redundantly validates
UUIDs with both express-validator (query('id').isUUID(4)) and the custom
isValidUuid checks inside the handlers; remove the inner runtime checks to keep
a single source of truth: delete the isValidUuid(id) guard in the
verifyScannedQR route handler (the block referencing isValidUuid(id) around
where id is set) and the similar isValidUuid(messId) guard in the getSignature
handler, relying on validationResult() from express-validator to enforce format;
if you prefer defense-in-depth instead, keep the checks but add a concise
comment above the isValidUuid calls stating they are intentional extra guards.
index.js (1)

177-185: Same NODE_ENV-shape mismatch on the SPA static-file fallback.

process.env.NODE_ENV === 'development' on line 179 won’t match dev, test, or local — the dev-mode index.html would only be served when NODE_ENV is exactly 'development'. Not a security issue (it just falls back to ./build/index.html), but for consistency consider using the same fail-secure pattern as elsewhere:

-    if (process.env.NODE_ENV === 'development') {
+    if (!IS_PRODUCTION) {
         p = path.resolve(__dirname, 'index.html')
     } else {
         p = path.resolve(__dirname, './build', 'index.html')
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 177 - 185, The SPA fallback currently serves dev
index.html only when process.env.NODE_ENV === 'development', which misses other
non-production env names; update the app.get('*') logic (the handler that sets p
and calls res.sendFile) to use a fail-secure pattern used elsewhere — either
treat anything not equal to 'production' as dev (e.g., process.env.NODE_ENV !==
'production') or explicitly check an allow-list (e.g.,
['development','dev','local','test'].includes(process.env.NODE_ENV)) — then
resolve p via path.resolve(__dirname, 'index.html') for dev envs and
path.resolve(__dirname, './build','index.html') for production before calling
res.sendFile(p).
LIVE_EXPOSURE_REPORT.md (1)

192-266: Add language identifiers to the fenced code blocks (markdownlint MD040).

The plain ``` fences on lines 192, 210, 218, 234, and 262 trip MD040 in CI. Most of them are HTTP responses / shell session output — the convention is text or http/console:

-```
+```http
 content-security-policy: default-src 'self';
 ...
-```
+```text
 200 200 200 200 200 ...

Apply the same to the M-4 / M-6 / reverse-proxy blocks. Pure formatting; no behavior change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LIVE_EXPOSURE_REPORT.md` around lines 192 - 266, Update the Markdown fenced
code blocks to include language identifiers to satisfy MD040: change the
content-security-policy block to ```http, the back-to-back request status line
block (the repeated "200 200 ...") to ```text, the M-4 example request/response
blocks (the POST/HTTP 400 and POST/HTTP 406 JSON responses) to ```http, the M-6
OpenAPI route listing block to ```text or ```http (prefer ```http if shown as
response body), and the reverse-proxy header block to ```http so CI linting
passes; locate these blocks by their contents (e.g., the content-security-policy
snippet, the repeated 200 statuses, the POST /api/auth/verifyLogin examples, the
/api-docs route list, and the x-powered-by/server headers) and replace the
triple-backtick fences accordingly.
middlewares/error.js (1)

39-46: sanitizeForClient regex covers the common cases but not every stack-frame shape.

The two patterns catch …/foo.js[:line:col] paths and at funcName ( … ) frames, which is the right 80% solution. Things that still pass through:

  • Native / anonymous frames: at Object.<anonymous> (no parens, no path).
  • V8-style frames without a function name: at /app/foo.js:12:34 (covered by the path regex but the surrounding at text remains).
  • Windows-style paths (C:\app\foo.js) — not relevant for the Alpine deployment but easy to forget.

For a stricter posture (and given that err.message content is largely operational and rarely useful to API clients), you could go further and just return a single generic string in production:

♻️ Stricter alternative
-function sanitizeForClient (msg) {
-    if (typeof msg !== 'string') return 'Error'
-    return msg
-        .replace(/\/[^\s'"`]+\.(js|ts|vue)(:\d+:\d+)?/g, '')
-        .replace(/\s+at\s+[^\s]+\s+\([^)]*\)/g, '')
-        .trim() || 'Error'
+// Allowlist of safe, user-actionable messages; everything else collapses to a
+// generic string so library internals never reach the client.
+const SAFE_MESSAGE = /^[A-Za-z0-9 ,.'!?:;()\-_/]{1,200}$/
+function sanitizeForClient (msg) {
+    if (typeof msg !== 'string') return 'Error'
+    const trimmed = msg.trim()
+    return SAFE_MESSAGE.test(trimmed) ? trimmed : 'Error'
+}

The current implementation is fine to ship; this is a hardening tightener if you want to close the long tail.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middlewares/error.js` around lines 39 - 46, The sanitizeForClient function
currently misses some stack-frame shapes; update sanitizeForClient to more
aggressively strip remaining stack/frame artifacts by: normalizing and removing
Windows-style paths (C:\...), stripping any leading "at ..." lines (including
frames like "at Object.<anonymous>" and bare "at /path:line:col"), removing bare
function-only frames and any leftover parenthesized sources, and collapsing the
result to a safe fallback ('Error'); optionally, if process.env.NODE_ENV ===
'production', always return the generic 'Error' string to ensure no internal
details leak. Ensure you modify the sanitizeForClient function to apply these
broader regexes/normalizations and the optional production shortcut.
apis/voters.js (1)

27-45: Use ethereumjs-tx's built-in getChainId() instead of hand-parsing the v byte.

ethereumjs-tx v1.3.7 exposes Transaction.prototype.getChainId() which returns 0 for unprotected transactions (v∈{27,28}) and the EIP-155 chainId otherwise—exactly the semantics your extractChainIdFromTx function recreates. Delegating to the library eliminates the v-byte parsing logic, reducing surface for edge cases (Buffer-vs-string conversion, leading zeros, sign handling) and ensures correctness if the library's internals change.

Suggested refactor
 let parsedTx
 try {
     parsedTx = new EthereumTx(serializedTx)
 } catch (e) {
     throw Error('rawTx is not a valid RLP-encoded transaction')
 }
 
 const expectedChainId = parseInt(config.get('blockchain.networkId'))
-const txChainId = extractChainIdFromTx(parsedTx)
+const txChainId = parsedTx.getChainId()  // 0 = unprotected, rejected by the check below
 if (!txChainId || txChainId !== expectedChainId) {
     throw Error(`rawTx chainId ${txChainId} does not match expected ${expectedChainId}`)
 }

You can drop extractChainIdFromTx entirely once migrated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apis/voters.js` around lines 27 - 45, The custom extractChainIdFromTx
function duplicates logic already provided by ethereumjs-tx; replace uses of
extractChainIdFromTx with the library call Transaction.prototype.getChainId
(i.e., call tx.getChainId() on the Transaction instance) and remove the
extractChainIdFromTx helper; ensure any callers that expected null/0 semantics
map tx.getChainId() results directly (it returns 0 for unprotected txs) and
remove Buffer/string parsing and try/catch logic tied to extractChainIdFromTx.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 31-32: The .env.example currently sets NODE_ENV=production which
will cause developers who copy it to unintentionally enable production-only
behavior governed by IS_PRODUCTION (HSTS preload, CSP changes, sanitized errors,
stricter rate limits, and Swagger gating); change the example to
NODE_ENV=development, add a short comment that production should be used only
for deployments and that SWAGGER_USER/SWAGGER_PASS must be set when
NODE_ENV=production, and optionally mention how to toggle IS_PRODUCTION for
CI/containers so local dev keeps verbose errors and Swagger access.

In `@apis/auth.js`:
- Around line 59-71: The current verification allows messages that contain
"id=<id>" but omit or fail to parse the `[XDCmaster <ts>]` prefix, letting
attackers bypass the TTL; update the verification in the function that checks
`message`, `id` and `tsMatch` so that the `[XDCmaster <timestamp>]` prefix is
required and its timestamp must parse to a number within LOGIN_SIGNATURE_TTL_MS
(i.e., if `tsMatch` is missing or Date.parse(ts) is NaN or outside the TTL,
throw an error like 'login signature expired/invalid'); ensure the check
enforces the canonical format emitted by `generateLoginQR` (match the exact
prefix and the `id=<id>` occurrence produced by that routine) so only correctly
formatted, fresh signatures pass.

In `@apis/ipfs.js`:
- Around line 129-143: The addKYC route currently reads credentials from
req.query (account, signedMessage, nonce) which can leak secrets; update the
router.post('/addKYC') handler to stop using req.query and only obtain account,
signedMessage and nonce from req.body and the x-kyc-* headers (via
normalizeValue) while keeping the same validation (presence checks,
UUID_V4_REGEX for nonce) and file upload check; adjust any variable
initialization that references req.query to use only req.body || req.headers and
leave normalizeValue, UUID_V4_REGEX and the unauthorized/badRequest branches
unchanged.
- Around line 196-210: The xinFinClient.add call uses an old callback pattern
and must be converted to the Promise/async-iterable API: await/for-await the
result of xinFinClient.add (or addAll) with fileBuffer, not a Node-style
callback; check the returned object fields (cid/path/size) not ipfsHash[0].hash,
defensively verify the result is defined before accessing properties, and wrap
the await in try/catch to return a 500 on failure; ensure uploaded.tempFilePath
is removed in a finally block (or after successful upload) to avoid leaking temp
files; also defer consuming the nonce (the code that atomically consumes the
nonce) until after the IPFS upload succeeds so a failed upload doesn't waste the
nonce. Use the existing symbols xinFinClient.add, fileBuffer,
uploaded.tempFilePath, and fileHash to locate and update the logic.

In `@apis/voters.js`:
- Around line 219-226: The catch block in generateQR currently writes e.message
directly to the response (res.send({...})), bypassing the centralized sanitizer
in middlewares/error.js; change the catch to call next(e) (or rethrow) instead
of sending the raw error so the existing error middleware can sanitize file
paths/stack traces before responding; also scan this file for any other catch
blocks that do res.send({error:{message: e.message}}) and convert them to use
next(e) for consistency with the rest of the routes.

In `@app/components/candidates/Apply.vue`:
- Around line 578-584: Before attempting to hash the uploaded file in Apply.vue
(the block using this.KYC.file.arrayBuffer() and window.crypto.subtle.digest to
compute fileHash), add an explicit check for window.crypto &&
window.crypto.subtle and early-fail with a clear user-facing error/toast (and
return) if absent; this prevents calling window.crypto.subtle.digest when
undefined, avoids consuming the nonce/continuing the flow, and surfaces a
helpful message instead of the generic TypeError.

In `@index.js`:
- Around line 85-99: DEBUG_REQUESTS is using the wrong env check and will be
true in mainnet/testnet/devnet; change its definition to reuse the existing
IS_PRODUCTION flag (invert it) so the request-logging middleware only runs when
not in non-production environments, e.g. derive DEBUG_REQUESTS from
IS_PRODUCTION and REQUEST_TRACE, and optionally make REQUEST_TRACE an explicit
opt-in (check for === '1' rather than !== '0') so the middleware around
logger.debug (the app.use block) only activates when intended.

In `@models/mongodb/index.js`:
- Around line 23-33: Replace the callback-style mongoose.connect(mongoUri, opts,
callback) with the promise-based form: call mongoose.connect(mongoUri, opts) and
handle success/failure with .then/.catch or await inside an async init flow so
connection errors are caught before models load; on failure call
logger.error(...) including the actual error and then process.exit(1). Update
the module bootstrap (or export an async init function) so that mongoose.connect
is awaited (using the same options: useNewUrlParser, useUnifiedTopology,
serverSelectionTimeoutMS) and ensure any code that registers/uses models runs
only after the awaited connect resolves.

In `@models/mongodb/ipfsNonce.js`:
- Around line 13-20: The IpfsNonce schema's nonce field should be made required
and the redundant index removed to avoid duplicate-index warnings and prevent
null-value uniqueness collisions: update the Schema definition for IpfsNonce so
the nonce property includes required: true and removes index: true (keep unique:
true), i.e., change the nonce field in the IpfsNonce schema to { type: String,
unique: true, required: true } to ensure validation rejects missing nonces and
eliminate the duplicate schema index warning.

In `@models/mongodb/signature.js`:
- Around line 8-11: The schema change to make signedId sparse won't update
existing MongoDB indexes; add a one-shot migration that drops the old signedId_1
index on the signatures collection and then calls
mongoose.model('Signature').syncIndexes() (run-once during deployment) so
MongoDB recreates the index as unique+sparse; implement the drop-and-sync logic
using the db.collection('signatures').dropIndex('signedId_1').catch(() => {})
pattern and then call syncIndexes(), and remove redundant index: true flags from
the signedAddress and signedId field definitions in the Signature model to avoid
duplicate/unused index declarations.

In `@SECURITY_FIXES.md`:
- Line 20: The table row in SECURITY_FIXES.md is broken by an unescaped pipe
inside the inline code span `v=27|28`; update that cell to escape the pipe so
GFM doesn't treat it as a column separator—for example replace the inline code
content with a safe alternative such as using the HTML entity or escaping the
pipe (e.g., `v=27&#124;28` or `v=27\|28`) in the same cell; ensure the rest of
the row text referencing `/verifyTx` and `apis/voters.js` remains unchanged so
the table renders with the correct four columns.

In `@test-harness/soak.sh`:
- Line 65: Update the misleading echo in soak.sh so the 429 count is attributed
to readLimiter (not authLimiter); change the message for the variable limited to
reference readLimiter and optionally txLimiter (if /api/voters/* is included)
and list the endpoints under test (/api/config, /, /api/candidates,
/api/transactions) to make it clear where the 429s originate; locate the echo
line that prints "429          : $limited ..." and update its text to mention
readLimiter (and txLimiter only as a conditional note).

---

Nitpick comments:
In `@apis/candidates.js`:
- Around line 938-999: The code redundantly validates UUIDs with both
express-validator (query('id').isUUID(4)) and the custom isValidUuid checks
inside the handlers; remove the inner runtime checks to keep a single source of
truth: delete the isValidUuid(id) guard in the verifyScannedQR route handler
(the block referencing isValidUuid(id) around where id is set) and the similar
isValidUuid(messId) guard in the getSignature handler, relying on
validationResult() from express-validator to enforce format; if you prefer
defense-in-depth instead, keep the checks but add a concise comment above the
isValidUuid calls stating they are intentional extra guards.

In `@apis/voters.js`:
- Around line 27-45: The custom extractChainIdFromTx function duplicates logic
already provided by ethereumjs-tx; replace uses of extractChainIdFromTx with the
library call Transaction.prototype.getChainId (i.e., call tx.getChainId() on the
Transaction instance) and remove the extractChainIdFromTx helper; ensure any
callers that expected null/0 semantics map tx.getChainId() results directly (it
returns 0 for unprotected txs) and remove Buffer/string parsing and try/catch
logic tied to extractChainIdFromTx.

In `@build/index.html`:
- Line 1: The PR accidentally commits generated frontend artifacts
(build/index.html and the hashed bundles like runtime.*.js, app.*.js,
vendor.*.js) which should not be versioned; remove the entire build/ directory
from the repo, add build/ to .gitignore, and update CI/CD to produce and publish
these artifacts during the deployment step (so consumers still get runtime.*,
node-vendor.*, app.*, style.* and vendor.* bundles) to avoid noisy diffs and
merge conflicts.

In `@Dockerfile`:
- Around line 41-48: The comment claims we avoid a recursive chown by using COPY
--chown, but the current runtime steps run npm install as root and then run
chown -R masternode:masternode node_modules, which negates that optimization;
fix by either (A) moving the USER masternode directive up before the runtime RUN
npm install so npm install runs as masternode (ensure /app is writable), or (B)
perform production install/prune in the build stage and COPY --from=build the
node_modules with --chown=masternode:masternode to the final image removing the
runtime npm install and chown, and if you intentionally keep the current
root-install-then-chown behavior, update the comment to reflect that fact
instead of claiming the chown is avoided (referencing RUN npm install, chown -R
masternode:masternode node_modules, USER masternode, and COPY --from=build
--chown).

In `@helpers/rateLimiters.js`:
- Around line 34-38: The authLimiter config sets message: 'Too many
authentication attempts.' which is never used because buildLimiter installs a
global custom handler that always returns the standard 429 payload; update
authLimiter by either removing the dead message property for consistency with
other limiters, or if you need a custom auth-specific message, modify
authLimiter to supply a per-limiter handler that returns your custom payload
(referencing authLimiter and buildLimiter/handler to locate the code) so the
auth message is actually sent.

In `@index.js`:
- Around line 177-185: The SPA fallback currently serves dev index.html only
when process.env.NODE_ENV === 'development', which misses other non-production
env names; update the app.get('*') logic (the handler that sets p and calls
res.sendFile) to use a fail-secure pattern used elsewhere — either treat
anything not equal to 'production' as dev (e.g., process.env.NODE_ENV !==
'production') or explicitly check an allow-list (e.g.,
['development','dev','local','test'].includes(process.env.NODE_ENV)) — then
resolve p via path.resolve(__dirname, 'index.html') for dev envs and
path.resolve(__dirname, './build','index.html') for production before calling
res.sendFile(p).

In `@LIVE_EXPOSURE_REPORT.md`:
- Around line 192-266: Update the Markdown fenced code blocks to include
language identifiers to satisfy MD040: change the content-security-policy block
to ```http, the back-to-back request status line block (the repeated "200 200
...") to ```text, the M-4 example request/response blocks (the POST/HTTP 400 and
POST/HTTP 406 JSON responses) to ```http, the M-6 OpenAPI route listing block to
```text or ```http (prefer ```http if shown as response body), and the
reverse-proxy header block to ```http so CI linting passes; locate these blocks
by their contents (e.g., the content-security-policy snippet, the repeated 200
statuses, the POST /api/auth/verifyLogin examples, the /api-docs route list, and
the x-powered-by/server headers) and replace the triple-backtick fences
accordingly.

In `@middlewares/error.js`:
- Around line 39-46: The sanitizeForClient function currently misses some
stack-frame shapes; update sanitizeForClient to more aggressively strip
remaining stack/frame artifacts by: normalizing and removing Windows-style paths
(C:\...), stripping any leading "at ..." lines (including frames like "at
Object.<anonymous>" and bare "at /path:line:col"), removing bare function-only
frames and any leftover parenthesized sources, and collapsing the result to a
safe fallback ('Error'); optionally, if process.env.NODE_ENV === 'production',
always return the generic 'Error' string to ensure no internal details leak.
Ensure you modify the sanitizeForClient function to apply these broader
regexes/normalizations and the optional production shortcut.

In `@sslcert/README.md`:
- Around line 11-14: Update the fenced code block in README.md that contains the
openssl command to include a language identifier; replace the opening triple
backticks with ```bash so the block starting with "openssl req -x509 -newkey
rsa:4096 -nodes -keyout server.key -out server.crt \ ..." is changed to a bash
fenced block for proper markdownlint (MD040) and GitHub rendering.
🪄 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

Run ID: a330a2e8-fbb9-466d-9cc2-9388febe62df

📥 Commits

Reviewing files that changed from the base of the PR and between 5376627 and 61fad27.

⛔ Files ignored due to path filters (4)
  • build/app.d6ee356cd701e09ed0fb.js.map is excluded by !**/*.map
  • build/node-vendor.d4d835193df6f3eb9196.js.map is excluded by !**/*.map
  • build/runtime.04647efee1a41c5e77ac.js.map is excluded by !**/*.map
  • build/style.9b2a3757cae03d13690f.js.map is excluded by !**/*.map
📒 Files selected for processing (38)
  • .env.example
  • .gitignore
  • Dockerfile
  • LIVE_EXPOSURE_REPORT.md
  • SECURITY_FIXES.md
  • apis/auth.js
  • apis/candidates.js
  • apis/index.js
  • apis/ipfs.js
  • apis/voters.js
  • app/app.js
  • app/components/Setting.vue
  • app/components/candidates/Apply.vue
  • build/app.b6d16146611612e84979.js
  • build/app.d6ee356cd701e09ed0fb.js
  • build/index.html
  • build/node-vendor.6a24ea3423f3be77c51c.js.LICENSE.txt
  • build/node-vendor.8099f31c7c5e3ac37d5a.js
  • build/node-vendor.8099f31c7c5e3ac37d5a.js.LICENSE.txt
  • build/node-vendor.d4d835193df6f3eb9196.js
  • build/runtime.04647efee1a41c5e77ac.js
  • build/runtime.f6886b9416edacf69bd3.js
  • build/style.85c65ce027f58e54d212.js
  • build/style.9b2a3757cae03d13690f.js
  • build/vendor.d9e3b7255f6258bc8ca1.js
  • build/vendor.dc6e866f897f3bdbedc9.js
  • helpers/rateLimiters.js
  • index.js
  • middlewares/error.js
  • models/mongodb/index.js
  • models/mongodb/ipfsNonce.js
  • models/mongodb/signature.js
  • package.json
  • sslcert/README.md
  • sslcert/server.crt
  • sslcert/server.key
  • test-harness/soak.sh
  • travis.pem.enc
💤 Files with no reviewable changes (4)
  • sslcert/server.crt
  • sslcert/server.key
  • build/vendor.d9e3b7255f6258bc8ca1.js
  • build/runtime.04647efee1a41c5e77ac.js

Comment thread .env.example Outdated
Comment thread apis/auth.js Outdated
Comment thread apis/ipfs.js
Comment thread apis/ipfs.js Outdated
Comment thread apis/voters.js
Comment thread models/mongodb/index.js
Comment thread models/mongodb/ipfsNonce.js
Comment thread models/mongodb/signature.js Outdated
Comment on lines +8 to +11
// unique+sparse keeps the lateral-takeover fix atomic: once any wallet has
// bound itself to a signedId the underlying unique-index error fires for
// every other concurrent wallet trying to claim the same QR session.
signedId: { type: String, unique: true, sparse: true, index: true },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no migration script for the signedId index ships in this PR.
fd -t f -e js -e ts . | xargs rg -nP '(syncIndexes|dropIndex).*signedId|signedId.*sparse' 2>/dev/null
rg -nP 'syncIndexes\s*\(' --type=js -C2

Repository: XinFinOrg/MasterNode-App

Length of output: 172


🏁 Script executed:

# Check for migration files or scripts
fd -t f \( -name '*migrat*' -o -name '*index*' \) . | head -20

Repository: XinFinOrg/MasterNode-App

Length of output: 299


🏁 Script executed:

# Look for SECURITY_FIXES.md or similar documentation
fd -t f \( -name 'SECURITY*' -o -name 'CHANGELOG*' -o -name 'MIGRATIONS*' \) .

Repository: XinFinOrg/MasterNode-App

Length of output: 299


🏁 Script executed:

# Read the full signature.js schema context
wc -l models/mongodb/signature.js

Repository: XinFinOrg/MasterNode-App

Length of output: 99


🏁 Script executed:

# Search for syncIndexes or index management in the entire codebase
rg -n 'syncIndexes|ensureIndex|createIndex|dropIndex' -t js -t ts --max-count=20

Repository: XinFinOrg/MasterNode-App

Length of output: 50


🏁 Script executed:

# Check if there are any initialization or startup scripts that manage indexes
rg -n 'Signature.*index|index.*Signature' -t js -t ts -C2 --max-count=15

Repository: XinFinOrg/MasterNode-App

Length of output: 50382


🏁 Script executed:

# Read the full signature.js file
cat -n models/mongodb/signature.js

Repository: XinFinOrg/MasterNode-App

Length of output: 937


🏁 Script executed:

# Search for migration or script directories
find . -type d \( -name migration -o -name migrations -o -name scripts -o -name db \) 2>/dev/null | head -20

Repository: XinFinOrg/MasterNode-App

Length of output: 137


🏁 Script executed:

# More targeted search for index operations in the codebase
rg 'dropIndex|syncIndexes|createIndex' --type js --type ts 2>/dev/null | head -20

Repository: XinFinOrg/MasterNode-App

Length of output: 50


🏁 Script executed:

# Check git log to understand what changed in signature.js
git log --oneline models/mongodb/signature.js 2>/dev/null | head -10

Repository: XinFinOrg/MasterNode-App

Length of output: 156


🏁 Script executed:

# List migration files
ls -la migrations/ 2>/dev/null || echo "migrations directory not readable"

Repository: XinFinOrg/MasterNode-App

Length of output: 326


🏁 Script executed:

# Search for migration files by pattern
find migrations -type f \( -name '*.js' -o -name '*.json' \) 2>/dev/null | head -20

Repository: XinFinOrg/MasterNode-App

Length of output: 135


🏁 Script executed:

# Check mongoose configuration for autoIndex settings
rg 'autoIndex|useCreateIndex|collection.*index' -t js -t ts --max-count=15

Repository: XinFinOrg/MasterNode-App

Length of output: 50382


🏁 Script executed:

# Search for any index-related initialization or schema setup
rg 'Signature.*Schema|Signature.*model|syncIndexes|ensureIndexes' -t js -t ts --max-count=15

Repository: XinFinOrg/MasterNode-App

Length of output: 50382


Production needs an explicit index migration — schema change alone won't flip existing index to sparse.

mongoose 5.x will not modify an already-existing index in MongoDB to add sparse: true; it only creates indexes that don't yet exist (and only when useCreateIndex is on, which is set in models/mongodb/index.js). On any environment that already ran with the previous non-sparse definition, the live signedId_1 index will remain non-sparse, so:

  • The lateral-takeover comment's atomicity guarantee actually relies on the old non-sparse unique index (which still rejects duplicate non-null values), but
  • Any historical row with signedId: null (or any future doc inserted before signedId is set) will now collide on the unique constraint, which the new schema implies should be allowed.

No MongoDB migration script exists in the repository. Ship a one-shot migration alongside this PR (drop signedId_1, then let mongoose / syncIndexes() recreate it as unique+sparse) and document it in SECURITY_FIXES.md:

await db.collection('signatures').dropIndex('signedId_1').catch(() => {})
await mongoose.model('Signature').syncIndexes()

Minor: with unique: true the explicit index: true is redundant on both signedAddress and signedId and can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/mongodb/signature.js` around lines 8 - 11, The schema change to make
signedId sparse won't update existing MongoDB indexes; add a one-shot migration
that drops the old signedId_1 index on the signatures collection and then calls
mongoose.model('Signature').syncIndexes() (run-once during deployment) so
MongoDB recreates the index as unique+sparse; implement the drop-and-sync logic
using the db.collection('signatures').dropIndex('signedId_1').catch(() => {})
pattern and then call syncIndexes(), and remove redundant index: true flags from
the signedAddress and signedId field definitions in the Signature model to avoid
duplicate/unused index declarations.

Comment thread SECURITY_FIXES.md Outdated
Comment thread test-harness/soak.sh Outdated
… major, 5 correctness, 9 nits)

Follow-up to PR XinFinOrg#49 covering every comment from the automated review:

Critical
- apis/ipfs.js: rewrite KYC pin to use ipfs-http-client@40+'s Promise API
  (the previous callback signature silently never fired and hung the
  request). Defer the atomic nonce consume until after the IPFS pin
  succeeds so a transient pinning failure no longer wastes the user's
  single-use nonce. Normalised v40/v50 result shapes; cleanup wrapped
  in finally{}.

Major
- apis/auth.js: require canonical "[XDCmaster <iso>] Login id=<uuid>"
  shape on /verifyLogin so attackers can't strip the prefix to skip
  the TTL window. Reject unparseable timestamps.
- apis/ipfs.js: drop req.query fallbacks for account/signedMessage/
  nonce — those leak credentials into nginx access logs, browser
  history and Referer headers. Body and x-kyc-* headers only.
- index.js: DEBUG_REQUESTS now keys off the file-wide IS_PRODUCTION
  fail-secure check (was inverted on mainnet|testnet|devnet) and
  REQUEST_TRACE is opt-in ('1') instead of opt-out. SPA fallback
  uses the same check for consistency.

Correctness
- apis/voters.js: generateQR catch now next(e) so sanitizeForClient
  runs (was sending raw e.message). Replaced hand-rolled
  extractChainIdFromTx with ethereumjs-tx@1's built-in getChainId()
  (verified semantically identical: 0 for v=27|28 / missing v).
- app/components/candidates/Apply.vue: explicit window.crypto.subtle
  availability check before digest() so plain-HTTP deployments fail
  with a readable toast instead of an opaque TypeError.
- models/mongodb/index.js: convert callback mongoose.connect to
  Promise form + connection.on('error') listener so connection
  failures surface and post-connect blips don't get swallowed.
- models/mongodb/signature.js: drop redundant index:true (unique
  already creates the index) — was emitting duplicate-index warnings.
- models/mongodb/ipfsNonce.js: nonce now required:true (a missing-
  field doc could match the consume CAS); same redundant-index
  cleanup.

Nits
- .env.example: NODE_ENV=development by default with comment that
  prod must override it (avoids tripping Swagger gating on local boot).
- helpers/rateLimiters.js: remove dead message field on authLimiter
  that the custom handler always overrode.
- Dockerfile: move USER masternode + chown /app ahead of npm install
  so we never run a recursive chown over node_modules. Comment now
  matches behaviour.
- apis/candidates.js: keep the doubled UUID validation as defense
  in depth with an explanatory comment.
- middlewares/error.js: tighten sanitizeForClient — strip Windows
  paths, multi-segment Unix paths, and dangling 'at' tokens, then
  gate the result through an explicit allowlist regex (/, <>, {},
  quotes, backticks excluded). Verified against 31 real validator/
  throw messages (all pass verbatim) and 8 dangerous shapes (all
  neutralised to 'Error' or path-stripped).
- SECURITY_FIXES.md: escape pipe in v=27|28 inline code so the
  markdown table row renders.
- test-harness/soak.sh: fix misleading 'authLimiter' label —
  the soak only hits read-side endpoints (readLimiter at 240
  req/min/IP).
- sslcert/README.md, LIVE_EXPOSURE_REPORT.md: add language
  identifiers (bash/http/text) on every fenced code block (MD040).

Also rebuilt the production webpack bundle so the Apply.vue
secure-context guard ships in build/. Lint passes clean on every
edited file (npm run lint).

Documented every finding + fix in SECURITY_FIXES.md under a new
"Code-review follow-ups — 2026-04-27" section.

Made-with: Cursor
@xcantera
Copy link
Copy Markdown
Author

Follow-up commit f624e7c — addresses every CodeRabbit comment

Pushed a single follow-up commit covering all 18 actionable findings from the automated review (1 critical, 3 major, 5 correctness, 9 nits). The triage matrix is reproduced in SECURITY_FIXES.md under the new "Code-review follow-ups — 2026-04-27" section, but here's the short version:

Critical

  • apis/ipfs.js — KYC pin rewritten to use ipfs-http-client@40+'s Promise API (the previous callback signature silently never fired and hung the request). Nonce consume is deferred until after the IPFS pin succeeds, so a transient pinning failure no longer wastes the user's single-use nonce. Cleanup wrapped in finally{}. v40/v50 result-shape normalisation included.

Major

  • apis/auth.jsverifyLogin now requires the canonical ^\[XDCmaster <iso>\] Login id=<uuid>$ shape, so attackers can't strip the prefix to skip the TTL window. Unparseable timestamps are rejected.
  • apis/ipfs.js — dropped req.query fallbacks for account / signedMessage / nonce (those leak credentials into nginx access logs, browser history, and Referer headers). Body + x-kyc-* headers only.
  • index.jsDEBUG_REQUESTS now keys off the file-wide IS_PRODUCTION fail-secure check (was inverted on mainnet|testnet|devnet) and REQUEST_TRACE is opt-in ('1') instead of opt-out. The SPA static-file fallback uses the same check for consistency.

Correctness

  • apis/voters.jsgenerateQR catch now next(e) so the central error middleware sanitises before responding; replaced hand-rolled extractChainIdFromTx with parsedTx.getChainId() (verified semantically identical: returns 0 for v=27|28 and missing v, the EIP-155-derived chainId otherwise).
  • app/components/candidates/Apply.vue — explicit window.crypto.subtle availability check before digest() so plain-HTTP deployments fail with a readable toast instead of an opaque TypeError.
  • models/mongodb/index.js — converted callback mongoose.connect to Promise form + connection.on('error') listener so connection failures surface and post-connect blips don't get swallowed.
  • models/mongodb/signature.js — dropped redundant index: true (was emitting duplicate-index warnings; unique: true already creates the index).
  • models/mongodb/ipfsNonce.jsnonce now required: true (a missing-field doc could match the consume CAS); same redundant-index cleanup.

Nits

  • .env.example — defaults to NODE_ENV=development with a comment that prod must override it (avoids tripping Swagger gating on local boot).
  • helpers/rateLimiters.js — removed dead message field on authLimiter that the custom handler always overrode.
  • Dockerfile — moved USER masternode + chown /app ahead of npm install; we never run a recursive chown over node_modules. Comment now matches behaviour.
  • apis/candidates.js — kept the doubled UUID validation as defense in depth with an explanatory comment.
  • middlewares/error.js — tightened sanitizeForClient: strip Windows paths, multi-segment Unix paths, and dangling at tokens, then gate the result through an explicit allowlist regex (no /, <>, {}, quotes, or backticks). Verified against 31 real validator/throw messages (all pass verbatim) and 8 dangerous shapes (all neutralised).
  • SECURITY_FIXES.md — escaped pipe in v=27|28 inline code so the markdown table row renders.
  • test-harness/soak.sh — corrected misleading authLimiter label (the soak only hits read-side endpoints — readLimiter at 240 req/min/IP).
  • sslcert/README.md, LIVE_EXPOSURE_REPORT.md — added language identifiers (bash/http/text) on every fenced code block (markdownlint MD040).

Verification

  • All edited JS files pass node --check.
  • npm run lint passes clean across every edited file (vue + js).
  • Webpack production build rebuilt; the new build/app.51a7fb9d79b73238fbbd.js ships the secure-context guard.
  • sanitizeForClient regression-tested against 31 legitimate message shapes (every one passes verbatim) and 8 attack-shape inputs (every one is either path-stripped or replaced with the generic "Error").

The single not-actioned comment is the meta-suggestion to remove build/ from version control: upstream master tracks the SPA bundle and the runtime image expects it; pulling that out would be a larger structural change best done in a separate PR by the maintainers.

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