Skip to content

Security: Fix rate limit bypass via X-Forwarded-For#1720

Merged
Scottcjn merged 2 commits intoScottcjn:mainfrom
yuzengbaao:fix/faucet-rate-limit-bypass
Mar 21, 2026
Merged

Security: Fix rate limit bypass via X-Forwarded-For#1720
Scottcjn merged 2 commits intoScottcjn:mainfrom
yuzengbaao:fix/faucet-rate-limit-bypass

Conversation

@yuzengbaao
Copy link
Contributor

Fixes an issue where faucet.py inherently fully trusts X-Forwarded-For from any host. We are preventing bypass of IP rate limiting by addressing spoofable HTTP headers and introducing independent rate limit restrictions based strictly on wallet addresses.

Closes rustchain-bounties#2246

Fixes an issue where the rustchain faucet trusts X-Forwarded-For from
a spoofable connection and allows bypassing IP rate limit.
Added rate limiting per wallet address in addition to IP address.
Copilot AI review requested due to automatic review settings March 21, 2026 03:55
@github-actions
Copy link

Welcome to RustChain! Thanks for your first pull request.

Before we review, please make sure:

  • Your PR has a BCOS-L1 or BCOS-L2 label
  • New code files include an SPDX license header
  • You've tested your changes against the live node

Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150)

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) size/S PR: 11-50 lines labels Mar 21, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to harden the faucet’s rate-limiting against spoofable client IP headers (notably X-Forwarded-For) and introduces plumbing intended to support wallet-based rate limiting.

Changes:

  • Stops using X-Forwarded-For for client IP determination and relies on request.remote_addr.
  • Generalizes rate-limit lookup helpers to support querying by either IP address or wallet identifier.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

faucet.py Outdated
Comment on lines 44 to 51
"""Get client IP address from request.

SECURITY: Only trust X-Forwarded-For from trusted reverse proxies.
Direct connections use remote_addr to prevent rate limit bypass via header spoofing.

SECURITY: Fix X-Forwarded-For spoofing. Verify proxy IP securely.
"""
remote = request.remote_addr or '127.0.0.1'
# Only trust forwarded headers from localhost (reverse proxy)
if remote in ('127.0.0.1', '::1') and request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
# Do not blindly trust X-Forwarded-For even from localhost to prevent spoofing
# If proxy is used, it should be configured to override remote_addr natively
return remote
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The docstring/comment says “Verify proxy IP securely”, but the implementation now ignores X-Forwarded-For entirely and relies only on request.remote_addr. Behind a reverse proxy this will typically be the proxy’s IP, causing all users to share one rate-limit bucket (operationally breaking the faucet). Either update the documentation to reflect the new behavior, or implement trusted-proxy handling (e.g., Werkzeug ProxyFix or an explicit allowlist of proxy source IPs and only then parse X-Forwarded-For).

Copilot uses AI. Check for mistakes.
faucet.py Outdated
Comment on lines +58 to +64
column = "wallet" if is_wallet else "ip_address"
c.execute(f'''
SELECT timestamp FROM drip_requests
WHERE ip_address = ?
WHERE {column} = ?
ORDER BY timestamp DESC
LIMIT 1
''', (ip_address,))
''', (identifier,))
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

get_last_drip_time builds the SQL using an f-string to inject the column name. Although column is currently derived from a boolean and therefore safe, this pattern is easy to accidentally extend with user-controlled input later and may be flagged by security tooling. Prefer two explicit, static queries (one for wallet, one for ip_address) to keep SQL fully parameterized and avoid dynamic SQL construction.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +72
def can_drip(identifier, is_wallet=False):
"""Check if the IP or Wallet can request a drip (rate limiting)."""
last_time = get_last_drip_time(identifier, is_wallet)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The PR description mentions adding an independent wallet-based rate limit, and the helpers now accept is_wallet, but the current codebase doesn’t appear to call can_drip(..., is_wallet=True) anywhere (only the IP path is used). As-is, this change introduces unused complexity without actually enforcing wallet-based limits. Either wire the wallet checks into the request handler (and provide a corresponding next_available for wallets) or remove the wallet-specific parameters until they’re enforced.

Copilot uses AI. Check for mistakes.
- Implement Werkzeug ProxyFix handling to securely resolve X-Forwarded-For instead of blindly ignoring or trusting it
- Remove dynamic SQL building (f-string) from SQLite queries by employing separate explicit queries for IP and Wallet checking
- Ensure wallet-based rate limiter restricts token dispensing successfully by patching the main drip route
- Ensure get_next_available() respects the is_wallet flag natively
@yuzengbaao
Copy link
Contributor Author

Thanks for the review. I have applied all changes.

  1. Safely handling X-Forwarded-For using werkzeug.middleware.proxy_fix.ProxyFix.
  2. Addressed dynamic SQL generation vulnerability by creating separate parameterized queries for wallet and IP bindings respectively.
  3. Updated the /faucet/drip routing view block to strictly track and halt on can_drip(wallet, is_wallet=True) to enforce independent wallet limit tracking.

@github-actions github-actions bot added the size/M PR: 51-200 lines label Mar 21, 2026
@Dlove123
Copy link

Code Review (Security-focused)

✅ Strengths

  1. Good fix for X-Forwarded-For spoofing
  2. Wallet-based rate limiting is a smart approach
  3. Closes the security gap properly

⚠️ Suggestions

  1. Consider adding rate limit headers to responses
  2. Add logging for rate limit triggers
  3. Consider exponential backoff for repeat offenders

🔒 Security Notes

  • This fix prevents IP spoofing attacks on the faucet
  • Wallet-based limiting is harder to bypass
  • Good defense-in-depth approach

Review Quality: Security-focused (15-25 RTC claim)

Claiming via rustchain-bounties#73

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) size/M PR: 51-200 lines size/S PR: 11-50 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants