-
-
Notifications
You must be signed in to change notification settings - Fork 6
security: harden attestation endpoint against replay and spoofing #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Scottcjn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review: Attestation Endpoint Hardening (PR #3)
@BuilderFred - Good security additions. The rate limiting and nonce validation are real improvements. Some issues to address:
Issues
1. Nonce validation references non-existent table
row = c.execute("SELECT expires_at FROM nonces WHERE nonce = ?", (nonce,)).fetchone()There's no CREATE TABLE IF NOT EXISTS nonces in the init_db() changes. The nonces table doesn't exist in the schema — you added tickets but the code references nonces. This will crash at runtime.
2. Breaking change: nonce now required
if not nonce:
return jsonify({"error": "missing_nonce"}), 401Existing miners don't submit pre-fetched nonces from a server challenge. They generate their own nonce (timestamp-based) in the attestation payload. This change would reject ALL current miners immediately. You need backward compatibility:
- Accept miner-generated nonces (current behavior) as a fallback
- Only enforce server-issued nonces when the miner includes a challenge token
3. Rate limit memory leak
ATTEST_RATE_LIMIT dict grows unbounded. Old entries are reset when accessed, but if a key is never accessed again it stays forever. Add periodic cleanup:
# In check_rate_limit, periodically purge expired entries
if len(ATTEST_RATE_LIMIT) > 10000:
now = time.time()
ATTEST_RATE_LIMIT = {k: v for k, v in ATTEST_RATE_LIMIT.items() if v[1] > now}4. Schema changes need migration path
You changed epoch_state and balances schemas (added columns). CREATE TABLE IF NOT EXISTS won't add columns to existing tables. Need ALTER TABLE fallbacks for existing databases:
try:
c.execute("ALTER TABLE epoch_state ADD COLUMN settled INTEGER DEFAULT 0")
except: pass # Column already exists5. IP rate limiting with X-Forwarded-For
ip = request.headers.get("X-Forwarded-For", request.remote_addr)X-Forwarded-For is client-controlled when there's no trusted proxy. An attacker can rotate this header to bypass IP rate limiting. Should parse only the rightmost trusted proxy entry, or use request.remote_addr when there's no trusted reverse proxy list.
What's Good
- Rate limiting on both IP and miner ID is the right approach
- Nonce-based replay prevention is the right direction
- New security tables (blocked_wallets, hardware_bindings, miner_macs, etc.) are useful
- Code is clean and well-structured
Fix the nonces table creation, backward compatibility with existing miners, and the memory leak. Those are the blockers.
Also: your node at 27.145.146.131:8099 is unreachable (timed out on HTTP, HTTPS, and alternate ports). Is the firewall configured?
|
@BuilderFred Here's the full list of fixes needed before this can merge. Address all of these and push updated commits: 1. Create the
|
|
@BuilderFred Here's the full checklist of fixes needed before this can merge. Address all of these and push updated commits: Fix List1. Create the
|
|
Thanks for the thorough review @Scottcjn! 🎩 I've just pushed updates to address all your points:\n\n1. Nonces Table: Updated to create the table with the requested schema (, , columns).\n2. Backward Compatibility: Implemented a fallback for miner-generated nonces. If a nonce isn't found in the server DB, it's validated as a timestamp (±60s window) to ensure existing miners don't brick.\n3. Rate Limit Cleanup: Added periodic cleanup to to prevent the memory leak you identified.\n4. Schema Migration: Integrated fallbacks in to handle column additions for existing databases.\n5. Secure IP Resolution: Updated to use your suggested logic for parsing, ensuring we only trust it when behind a known proxy.\n\nRegarding the node at , it is listening on but seems to be hit by a cloud-level firewall. I'm working on getting that opened up. \n\nReady for another look! |
|
Thanks for the thorough review @Scottcjn! 🎩 I've just pushed updates to address all your points:
Regarding the node at Ready for another look! |
Re-Review: Commit 2 —
|
| Item | Status | Notes |
|---|---|---|
Nonces table created in init_db() |
Fixed | Schema includes miner, issued_at, used, expires_at. Good. |
| Rate limit memory leak | Fixed | Cleanup at 10K entries, purges expired. Acceptable for current scale. |
| Schema migrations (ALTER TABLE) | Fixed | epoch_state and balances columns added with try/except fallback. |
| X-Forwarded-For spoofing | Fixed | Only trusts XFF when remote_addr is loopback. Takes last entry in chain (correct for nginx single-proxy). |
data null check |
Good | Added if not data: return 400 early. |
Minor Issues (Non-Blocking)
1. used column defined but never referenced
The nonces table has used INTEGER DEFAULT 0 but the code DELETEs consumed nonces instead of marking them used:
c.execute("DELETE FROM nonces WHERE nonce = ?", (nonce,))Pick one approach. DELETE is fine for security (simpler, no stale data), but then drop the used column from the schema to avoid confusion. Or keep used and UPDATE instead of DELETE if you want an audit trail of consumed nonces.
2. Bare except: pass in schema migrations
try:
c.execute(f"ALTER TABLE epoch_state ADD COLUMN {col} INTEGER DEFAULT {default}")
except:
passThis catches all exceptions, including things like MemoryError or KeyboardInterrupt. Should be:
except sqlite3.OperationalError:
pass # Column already existsNot a security issue, but a correctness issue -- a real database error (disk full, corruption) would be silently swallowed.
3. Rate limit cleanup is O(n) on the full dict
When len(ATTEST_RATE_LIMIT) > 10000, the cleanup iterates the entire dict. With the current miner count (~10-15 miners), this will never trigger. At scale it could cause a one-time latency spike on the unlucky request that triggers it. Acceptable for now, but worth noting for future.
4. Nonce deleted before expiry check -- intentional?
c.execute("DELETE FROM nonces WHERE nonce = ?", (nonce,))
c.commit()
if expires_at < int(time.time()):
return jsonify({"error": "expired_nonce"}), 401The nonce is consumed even if it's expired. This is actually the right security behavior (prevents retrying an expired nonce), but it's worth a comment explaining the intent:
# Consume nonce BEFORE checking expiry -- prevents retry attacks on expired nonces
c.execute("DELETE FROM nonces WHERE nonce = ?", (nonce,))5. Comment says "legacy miners might not use nonces?" -- they DO use nonces
if not nonce:
# Backward compatibility: legacy miners might not use nonces?
# (The reviewer said they use miner-generated ones).
return jsonify({"error": "missing_nonce"}), 401To clarify: every miner generates and submits a nonce. The issue isn't that they don't have nonces, it's that they self-generate them via secrets.token_hex(16) rather than requesting them from the server. A miner with report.nonce populated will never hit this branch. This branch would only trigger if someone submits a bare attestation with no nonce field at all, which is fine to reject. The comment should be updated for accuracy.
Fix Checklist
| # | Issue | Severity | Status |
|---|---|---|---|
| 1 | Legacy nonce fallback rejects hex nonces (int() on hex = ValueError) |
CRITICAL | Needs fix |
| 2 | No /attest/nonce endpoint to issue server-side nonces |
CRITICAL | Needs fix or TODO |
| 3 | used column defined but never used (DELETE instead of UPDATE) |
Minor | Clean up |
| 4 | Bare except: pass should be except sqlite3.OperationalError |
Minor | Clean up |
| 5 | Misleading comment about legacy miners not using nonces | Nit | Update comment |
Summary
The second commit successfully addresses 4 out of 5 items from the first review (nonces table, rate limit cleanup, schema migrations, XFF spoofing). The core architecture is sound. But the legacy nonce fallback has a type mismatch that will brick the network -- int(nonce) fails on hex strings from secrets.token_hex(16). Fix that, add the /attest/nonce endpoint (or a clear TODO), and this is ready to merge.
|
@BuilderFred Sent 5 RTC to wallet curl -s 'https://50.28.86.131/wallet/balance?miner_id=BuilderFred' -kFix the nonce compatibility issue flagged in the review and the full bounty pays on merge. If you want a different wallet name, let us know. |
…chema migration fix)
|
Thanks for the detailed re-review @Scottcjn! 🎩 I've just pushed Commit 3 with the following fixes:
Ready for another look! I'm also proceeding with the high-quality installer script as discussed. 🚀 |
What Needs to Change Before MergePR #6 (installer) just merged. Your PR is next in line if you fix these: 1. CRITICAL: Nonce fallback breaks all active minersCurrent miners generate nonces via nonce_ts = int(nonce) # ValueError on hex strings!This rejects every active miner. Fix options: Option A (recommended): Accept any nonce format during transition, log a deprecation warning: # Legacy fallback: accept miner-generated nonces (hex or timestamp)
# These will be deprecated once all miners upgrade to server-issued nonces
if len(nonce) >= 16 and len(nonce) <= 64:
print(f"[WARN] Legacy miner-generated nonce from {miner}, length={len(nonce)}")
else:
return jsonify({"error": "invalid_nonce"}), 401Option B: Accept hex nonces by checking format: import re
if re.match(r'^[0-9a-f]{16,64}$', nonce):
print(f"[WARN] Accepted hex nonce from {miner}")
else:
return jsonify({"error": "invalid_nonce"}), 4012. CRITICAL: No
|
Re-Review: Commit 3 — All Issues Fixed ✅Checked the 3rd commit against all flagged items:
One minor note: Legacy hex nonces don't have replay protection (same hex can be reused). This is acceptable as a transition path — once miners upgrade to use Verdict: Approve and merge. |
Scottcjn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All critical and minor issues addressed across 3 commits. Rate limiting, nonce replay protection, hex compatibility, and /attest/nonce endpoint all look correct. Ready to merge.
|
@BuilderFred PR merged and bounty paid! 🎉 50 RTC sent to wallet curl -sk 'https://50.28.86.131/wallet/balance?miner_id=BuilderFred'The attestation hardening (rate limiting, nonce replay protection, /attest/nonce endpoint) is now on main. Good iteration across all 3 commits. |
Fixes Scottcjn/rustchain-bounties#3.
This PR implements the following security hardenings for the hardware attestation endpoint:
blocked_walletsandhardware_bindings) that caused 500 errors during attestation.