Skip to content

Conversation

@elavol
Copy link

@elavol elavol commented Dec 31, 2025

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

This resolve issue #457

Summary by CodeRabbit

  • Configuration Changes

    • Replaced the Cloudflare-only proxy flag with a new CLIENT_IP_HEADER. IP detection now auto-scans multiple common proxy headers and falls back to a specified header if set, improving reliability behind various proxies.
  • Documentation

    • Updated self-hosting and configuration guides to explain the new header-based IP configuration and how to set CLIENT_IP_HEADER for your proxy environment.

✏️ Tip: You can customize this high-level summary in your review settings.

@CLAassistant
Copy link

CLAassistant commented Dec 31, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

Replaces a Cloudflare-specific proxy flag with a configurable header-based approach: backend reads a new CLIENT_IP_HEADER (optional) and falls back to a prioritized list of common proxy headers to extract the client IP, with special parsing for x-forwarded-for, forwarded, and cloudfront-viewer-address.

Changes

Cohort / File(s) Summary
Configuration
backend/.env.example
Added CLIENT_IP_HEADER variable and explanatory comments for proxy headers (Cloudflare cf-connecting-ip, CloudFront cloudfront-viewer-address, etc.).
Constants
backend/apps/community/src/common/constants.ts
Removed exported isProxiedByCloudflare and its internal CLOUDFLARE_PROXY_ENABLED-based definition.
IP Header Detection Logic
backend/apps/community/src/common/utils.ts
Replaced Cloudflare flag usage with an IP_ADDRESS_HEADERS list; refactored getIPFromHeaders to honor CLIENT_IP_HEADER (if valid), scan prioritized headers, and handle x-forwarded-for, forwarded, and cloudfront-viewer-address parsing rules.
Documentation
docs/docs/selfhosting/configuring.md, docs/docs/selfhosting/how-to.md
Replaced CLOUDFLARE_PROXY_ENABLED guidance with CLIENT_IP_HEADER instructions and updated text to recommend a generic, configurable proxy header approach.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Proxy as CDN/Proxy
  participant Server
  Note over Client,Proxy: HTTP request may pass through a proxy that adds headers
  Client->>Proxy: HTTP Request (source IP)
  Proxy->>Server: HTTP Request with proxy headers (e.g., x-forwarded-for, cf-connecting-ip, cloudfront-viewer-address)
  Server->>Server: getIPFromHeaders(headers)
  alt CLIENT_IP_HEADER set and present
    Server->>Server: return headers[CLIENT_IP_HEADER]
  else header found in IP_ADDRESS_HEADERS
    Server->>Server: if x-forwarded-for -> take first IP
    Server->>Server: if forwarded -> extract for= value
    Server->>Server: if cloudfront-viewer-address -> strip port
    Server->>Server: return extracted IP
  else no header
    Server->>Server: return null
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
I sniff the headers, soft and fleet,
A hop to find the client's feet,
From CF, CloudFront, or a custom name,
One header guides me to the game—🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description follows the required template with most sections completed, though it lacks details about the specific changes made and unchecks the documentation update despite docs being modified. Add a 'Changes' section describing what was modified (e.g., replaced CLOUDFLARE_PROXY_ENABLED with CLIENT_IP_HEADER, updated IP detection logic). Check the documentation checkbox if docs were updated.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: replacing Cloudflare-specific proxy handling with support for multiple proxy headers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f98f287 and 1860838.

📒 Files selected for processing (1)
  • backend/apps/community/src/common/utils.ts
🔇 Additional comments (2)
backend/apps/community/src/common/utils.ts (2)

122-136: LGTM! Comprehensive proxy header list.

The IP_ADDRESS_HEADERS constant provides good coverage of common CDN and reverse proxy headers, with an intentional priority order that favors CDN-specific headers before falling back to generic ones.


1170-1176: LGTM! Clean header detection logic.

The use of Array.find to locate the first present header from the priority list is clear and correct, with an appropriate null return when no headers match.


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

@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: 5

🧹 Nitpick comments (3)
docs/docs/selfhosting/configuring.md (1)

37-37: Clarify the documentation for CLIENT_IP_HEADER.

The description should be more specific about:

  • Which header name to provide (e.g., "Set to the name of the proxy header containing the client IP, such as cf-connecting-ip or x-forwarded-for")
  • What happens when not set (automatic detection using common proxy headers)
  • Whether the default is truly null or empty/unset
Suggested improvement
-| `CLIENT_IP_HEADER`         | `null`                                 | If set - Swetrix will get IP address information from header provided by Proxy.                                                                                                                                  |
+| `CLIENT_IP_HEADER`         |                                        | The name of the proxy header to use for determining client IP addresses (e.g., `cf-connecting-ip`, `x-forwarded-for`). If not set, Swetrix will automatically detect the IP from common proxy headers.           |
backend/.env.example (1)

84-88: Fix typo in comment.

"Cloudfront" should be "CloudFront" (capital F) for consistency with the official product name.

🔎 Proposed fix
 # Configure proxy header for client IP
 # Some well know proxies
 # Cloudflare: cf-connecting-ip
-# Cloudfront: cloudFront-viewer-address
+# CloudFront: cloudfront-viewer-address
 CLIENT_IP_HEADER=cf-connecting-ip
backend/apps/community/src/common/utils.ts (1)

1184-1190: Consider improving forwarded header parsing.

The regex handles basic IPv4 and IPv6 patterns, but the extracted IPv6 address may include brackets that should be stripped for consistency.

🔎 Proposed improvement
   if (header === 'forwarded') {
     const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/)
 
     if (match) {
-      return match[1]
+      // Remove brackets from IPv6 addresses
+      return match[1].replace(/[\[\]]/g, '')
     }
   }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0c4c52 and ff8a84c.

📒 Files selected for processing (5)
  • backend/.env.example
  • backend/apps/community/src/common/constants.ts
  • backend/apps/community/src/common/utils.ts
  • docs/docs/selfhosting/configuring.md
  • docs/docs/selfhosting/how-to.md
💤 Files with no reviewable changes (1)
  • backend/apps/community/src/common/constants.ts
🔇 Additional comments (1)
backend/apps/community/src/common/utils.ts (1)

122-136: Good coverage of proxy headers.

The list provides comprehensive coverage of common proxy and CDN headers with reasonable precedence ordering.

Comment on lines 1170 to 1198
const header = IP_ADDRESS_HEADERS.find(name => {
return headers.get(name)
})

if (ip) {
return ip
if (!header) {
return null
}

ip = headers['x-forwarded-for'] || null
const ip = headers.get(header)

if (!ip) {
return null
if (header === 'x-forwarded-for') {
return ip.split(',')[0].trim()
}

return _split(ip, ',')[0]
if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/)

if (match) {
return match[1]
}
}
if (header === 'cloudfront-viewer-address') {
const lastColonIndex = ip.lastIndexOf(':')
if (lastColonIndex > 0) {
return ip.substring(0, lastColonIndex)
}
}
return ip
}
Copy link

@coderabbitai coderabbitai bot Dec 31, 2025

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:

cat -n backend/apps/community/src/common/utils.ts | sed -n '1150,1210p'

Repository: Swetrix/swetrix

Length of output: 1731


🏁 Script executed:

rg -n "getIPFromHeaders" backend/apps/community/src/ -g '!**/test/**' -g '!**/*.spec.ts' -A 3 -B 1

Repository: Swetrix/swetrix

Length of output: 8733


🏁 Script executed:

rg -n "validateIP\|isValidIP\|ipv4\|ipv6" backend/apps/community/src/ -g '!**/test/**' -g '!**/*.spec.ts' -i

Repository: Swetrix/swetrix

Length of output: 41


🏁 Script executed:

rg -n "const checkRateLimit\|function checkRateLimit" backend/apps/community/src/common/utils.ts -A 20

Repository: Swetrix/swetrix

Length of output: 41


🏁 Script executed:

rg -n "checkRateLimit" backend/apps/community/src/ --type=ts -g '!**/test/**' -g '!**/*.spec.ts' | head -5

Repository: Swetrix/swetrix

Length of output: 580


🏁 Script executed:

rg -n "checkRateLimit" backend/apps/community/src/common/utils.ts -B 2 -A 15

Repository: Swetrix/swetrix

Length of output: 1155


🏁 Script executed:

grep -n "export.*checkRateLimit" backend/apps/community/src/common/utils.ts

Repository: Swetrix/swetrix

Length of output: 41


🏁 Script executed:

rg -n "getRateLimitHash" backend/apps/community/src/common/utils.ts -B 2 -A 8

Repository: Swetrix/swetrix

Length of output: 806


🏁 Script executed:

rg -n "IP_ADDRESS_HEADERS" backend/apps/community/src/common/utils.ts -B 2 -A 2

Repository: Swetrix/swetrix

Length of output: 642


The custom header case returns completely unvalidated IP values; consider sanitizing or validating before use.

The function lacks explicit IP format validation. Most critically, at line 1167, a custom CLIENT_IP_HEADER value is returned raw without any sanitization. Additionally, the regex pattern for the 'forwarded' header (line 1185) does not properly match standard IPv4 addresses (e.g., "192.168.1.1" contains no colons and would not match). The extracted IP is passed directly to checkRateLimit(), which only hashes it without validation—acceptable for rate limiting by hash, but the unvalidated custom header poses a risk if the header is attacker-controlled.

🤖 Prompt for AI Agents
In backend/apps/community/src/common/utils.ts around lines 1170 to 1198, the
function returns header-derived IPs without validation (including the custom
CLIENT_IP_HEADER) and the 'forwarded' regex only matches addresses with colons;
update the code to validate and sanitize any extracted IP before returning it:
use Node's net.isIP (or equivalent) to accept only valid IPv4/IPv6 addresses
(return null or fallback when invalid), normalize/trim values, enhance the
'forwarded' regex to also capture IPv4 (e.g., for=192.168.0.1 and
for="[::1]":port), properly strip optional port information (including bracketed
IPv6 ports), and apply the same validation to the cloudfront and x-forwarded-for
branches so attacker-controlled headers can't produce unvalidated IPs.

Copy link
Author

Choose a reason for hiding this comment

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

@Blaumaus Im not sure this is something mandatory, the proxies usually provide correct ip address, so IMO we dont really need to verify header value. What's your though?

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@Blaumaus Blaumaus self-assigned this Dec 31, 2025
@Blaumaus
Copy link
Member

good PR. I'll review & merge it later, it will be included in the next release

Copy link

@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: 1

Fix all issues with AI Agents 🤖
In @backend/apps/community/src/common/utils.ts:
- Around line 1184-1190: The current forwarded-header parsing block (checking
header === 'forwarded') uses the regex /for=(\[?[0-9a-fA-F:.]+\]?)/ which fails
for quoted tokens and leaves port suffixes; update the logic so you first
extract the token after for= allowing optional surrounding double quotes (e.g.
for="...") and then normalize it by removing an optional port suffix (handle
both bracketed IPv6 like [2001:db8::1]:8080 and unbracketed IPv4:port), finally
return the bare address. Locate the block using variables header, ip and match,
replace the single-group regex/return with a two-step extract-and-normalize
approach (capture optional quotes, strip quotes if present, strip trailing :port
only when it is outside a trailing ']' for IPv6) and return the cleaned address
instead of match[1].
♻️ Duplicate comments (2)
backend/apps/community/src/common/utils.ts (2)

1191-1196: IPv6 handling still broken for cloudfront-viewer-address.

This was flagged in a previous review and remains unaddressed. CloudFront uses [ipv6]:port format for IPv6 addresses. Using lastIndexOf(':') will:

  • For [2001:db8::1]:443 → return [2001:db8::1] (includes brackets, invalid IP)
  • For unbracketed IPv6 without port → corrupt the address
🔎 Proposed fix
   if (header === 'cloudfront-viewer-address') {
+    // CloudFront format: "ipv4:port" or "[ipv6]:port"
+    if (ip.startsWith('[')) {
+      const bracketEnd = ip.indexOf(']')
+      if (bracketEnd > 0) {
+        return ip.substring(1, bracketEnd)
+      }
+    }
     const lastColonIndex = ip.lastIndexOf(':')
-    if (lastColonIndex > 0) {
+    // Only strip port for IPv4 (single colon)
+    if (lastColonIndex > 0 && ip.indexOf(':') === lastColonIndex) {
       return ip.substring(0, lastColonIndex)
     }
   }

1162-1167: Unvalidated custom header value returned directly.

When CLIENT_IP_HEADER is set, its value is returned without any IP format validation. If this header is attacker-controlled or misconfigured, it could contain arbitrary data that gets passed to downstream functions like getGeoDetails() which expect a valid IP address.

Consider validating with Node's net.isIP() before returning:

🔎 Proposed fix
+import { isIP } from 'net'
+
 const getIPFromHeaders = (headers: any) => {
   const customHeader = process.env.CLIENT_IP_HEADER
 
   // only use custom header if not present in IP_ADDRESS_HEADERS
   // since we might need some extra process based on proxy
   if (
     !!customHeader &&
     !IP_ADDRESS_HEADERS.includes(customHeader) &&
     headers.get(customHeader)
   ) {
-    return headers.get(customHeader)
+    const ip = headers.get(customHeader)?.trim()
+    return isIP(ip) ? ip : null
   }
🧹 Nitpick comments (2)
backend/.env.example (1)

81-85: Approve the new CLIENT_IP_HEADER configuration approach.

The replacement of CLOUDFLARE_PROXY_ENABLED with the configurable CLIENT_IP_HEADER is a good design. The comments clearly document supported proxy headers and provide useful examples.

Optional: Standardize header name casing in comments.

For documentation clarity, consider using the standard AWS casing for the CloudFront header name (e.g., CloudFront-Viewer-Address instead of cloudFront-viewer-address). While HTTP headers are case-insensitive, consistent casing in documentation improves clarity.

🔎 Proposed improvement
  # Configure proxy header for client IP
  # Some well know proxies
  # Cloudflare: cf-connecting-ip
- # Cloudfront: cloudFront-viewer-address
+ # CloudFront: CloudFront-Viewer-Address
  CLIENT_IP_HEADER=cf-connecting-ip
backend/apps/community/src/common/utils.ts (1)

1170-1198: Consider centralizing IP validation for all extraction paths.

The function has multiple return paths (custom header, x-forwarded-for, forwarded, cloudfront, default) but none validate the extracted IP. While the custom header case is most concerning, all paths could benefit from a final validation step before returning.

🔎 Suggested pattern
import { isIP } from 'net'

const getIPFromHeaders = (headers: any): string | null => {
  let ip: string | null = null
  
  // ... existing extraction logic setting `ip` ...
  
  // Final validation
  if (ip) {
    ip = ip.trim()
    return isIP(ip) ? ip : null
  }
  return null
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff8a84c and f98f287.

📒 Files selected for processing (3)
  • backend/.env.example
  • backend/apps/community/src/common/utils.ts
  • docs/docs/selfhosting/how-to.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/selfhosting/how-to.md
🔇 Additional comments (3)
backend/.env.example (1)

81-85: Note: CLOUDFLARE_PROXY_ENABLED removal acknowledged.

The deprecated CLOUDFLARE_PROXY_ENABLED variable is not present in the current configuration, indicating it has been properly removed as flagged in the prior review. This aligns with the migration to the header-based approach.

backend/apps/community/src/common/utils.ts (2)

23-23: LGTM!

Clean removal of the isProxiedByCloudflare import, consistent with the new header-based approach.


122-136: LGTM!

Good header priority order - CDN-specific headers (Cloudfront, Cloudflare, Fastly) are checked before generic forwarding headers. The inline comments documenting each header's origin improve maintainability.

Comment on lines +1184 to +1190
if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/)

if (match) {
return match[1]
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete RFC 7239 forwarded header parsing.

The regex /for=(\[?[0-9a-fA-F:.]+\]?)/ doesn't handle:

  1. Quoted values per RFC 7239: for="192.168.1.1" or for="[2001:db8::1]:port"
  2. Port stripping from bracketed IPv6: for=[2001:db8::1]:8080

This may cause null returns or include port suffixes for some proxy configurations.

🔎 Proposed fix
   if (header === 'forwarded') {
-    const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/)
-
-    if (match) {
-      return match[1]
+    // Handle both quoted and unquoted formats per RFC 7239
+    // for=192.168.1.1, for="192.168.1.1", for=[::1], for="[::1]:port"
+    const match = ip.match(/for="?\[?([0-9a-fA-F.:]+)\]?(?::\d+)?"?/i)
+    if (match) {
+      return match[1]
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/)
if (match) {
return match[1]
}
}
if (header === 'forwarded') {
// Handle both quoted and unquoted formats per RFC 7239
// for=192.168.1.1, for="192.168.1.1", for=[::1], for="[::1]:port"
const match = ip.match(/for="?\[?([0-9a-fA-F.:]+)\]?(?::\d+)?"?/i)
if (match) {
return match[1]
}
}
🤖 Prompt for AI Agents
In @backend/apps/community/src/common/utils.ts around lines 1184-1190, The
current forwarded-header parsing block (checking header === 'forwarded') uses
the regex /for=(\[?[0-9a-fA-F:.]+\]?)/ which fails for quoted tokens and leaves
port suffixes; update the logic so you first extract the token after for=
allowing optional surrounding double quotes (e.g. for="...") and then normalize
it by removing an optional port suffix (handle both bracketed IPv6 like
[2001:db8::1]:8080 and unbracketed IPv4:port), finally return the bare address.
Locate the block using variables header, ip and match, replace the single-group
regex/return with a two-step extract-and-normalize approach (capture optional
quotes, strip quotes if present, strip trailing :port only when it is outside a
trailing ']' for IPv6) and return the cleaned address instead of match[1].

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.

3 participants