Skip to content

Commit 39e595c

Browse files
cfsmp3claude
andauthored
security: validate trusted proxy before trusting IP headers (#410)
* security: validate trusted proxy before trusting IP headers Only trust X-Real-IP and X-Forwarded-For headers when the request originates from a trusted proxy, preventing rate limit bypass via spoofed headers. Trusted proxies: - Loopback addresses (nginx on same server) - IPs specified in TRUSTED_PROXIES env var (comma-separated, CIDR supported) - Docker bridge network (172.16.0.0/12) in production For untrusted connections, use RemoteAddr directly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add TRUSTED_PROXIES documentation to READMEs Addresses review feedback to document the TRUSTED_PROXIES environment variable usage in both backend/README.md and production/README.md. Explains: - Automatic trust for loopback and Docker networks - Manual configuration via TRUSTED_PROXIES env var - When to use in different deployment scenarios Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 848956b commit 39e595c

3 files changed

Lines changed: 111 additions & 12 deletions

File tree

backend/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@
5757
If you are running the backend via Docker, the exposed ports are determined by the compose configuration. To use a different port in a Docker environment, you must manually update the docker-compose.yml file to adjust the container’s port mapping.
5858
Also, if you change `CCSYNC_PORT`, remember to update `CONTAINER_ORIGIN` accordingly.
5959

60+
### Rate Limiting and Trusted Proxies
61+
62+
The backend includes rate limiting that uses the client's IP address. When running behind a reverse proxy (like nginx), you need to configure trusted proxies so the backend correctly identifies client IPs from proxy headers.
63+
64+
**Automatic Trust:**
65+
- Loopback addresses (127.0.0.1, ::1) are always trusted
66+
- In production (`ENV=production`), Docker bridge networks (172.16.0.0/12) are trusted
67+
68+
**Manual Configuration:**
69+
70+
Use `TRUSTED_PROXIES` to specify additional trusted proxy IPs or CIDR ranges:
71+
72+
```bash
73+
# Single IP
74+
TRUSTED_PROXIES="10.0.0.1"
75+
76+
# Multiple IPs (comma-separated)
77+
TRUSTED_PROXIES="10.0.0.1,10.0.0.2"
78+
79+
# CIDR notation
80+
TRUSTED_PROXIES="10.0.0.0/8,192.168.0.0/16"
81+
```
82+
83+
**When to use:**
84+
- **Local development**: Not needed (loopback is trusted by default)
85+
- **Production with nginx on same server**: Not needed (loopback is trusted)
86+
- **Production with external load balancer**: Set to your load balancer's IP/range
87+
6088
- Run the application:
6189

6290
```bash

backend/middleware/ratelimit.go

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package middleware
22

33
import (
44
"fmt"
5+
"net"
56
"net/http"
7+
"os"
68
"strings"
79
"sync"
810
"time"
@@ -103,23 +105,86 @@ func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
103105
}
104106
}
105107

106-
func getRealIP(r *http.Request) string {
107-
ip := r.Header.Get("X-Real-IP")
108-
if ip != "" {
109-
return ip
108+
// isTrustedProxy checks if the request is from a trusted proxy.
109+
// In production, we trust localhost connections (nginx running on same server).
110+
// The TRUSTED_PROXIES env var can specify additional trusted proxy IPs (comma-separated).
111+
func isTrustedProxy(remoteAddr string) bool {
112+
// Extract IP from remoteAddr (format: "ip:port" or just "ip")
113+
host, _, err := net.SplitHostPort(remoteAddr)
114+
if err != nil {
115+
host = remoteAddr
116+
}
117+
118+
// Parse the IP
119+
ip := net.ParseIP(host)
120+
if ip == nil {
121+
return false
122+
}
123+
124+
// In production, trust loopback (nginx on same server)
125+
if ip.IsLoopback() {
126+
return true
110127
}
111128

112-
ip = r.Header.Get("X-Forwarded-For")
113-
if ip != "" {
114-
ips := strings.Split(ip, ",")
115-
if len(ips) > 0 {
116-
return strings.TrimSpace(ips[0])
129+
// Check against TRUSTED_PROXIES env var
130+
trustedProxies := os.Getenv("TRUSTED_PROXIES")
131+
if trustedProxies != "" {
132+
for _, trusted := range strings.Split(trustedProxies, ",") {
133+
trusted = strings.TrimSpace(trusted)
134+
// Support CIDR notation
135+
if strings.Contains(trusted, "/") {
136+
_, network, err := net.ParseCIDR(trusted)
137+
if err == nil && network.Contains(ip) {
138+
return true
139+
}
140+
} else {
141+
trustedIP := net.ParseIP(trusted)
142+
if trustedIP != nil && trustedIP.Equal(ip) {
143+
return true
144+
}
145+
}
146+
}
147+
}
148+
149+
// In Docker environments, common bridge networks
150+
// 172.17.0.0/16 is the default Docker bridge network
151+
// 10.0.0.0/8 is commonly used for internal networks
152+
if os.Getenv("ENV") == "production" {
153+
// Trust Docker internal networks in production
154+
dockerBridge := &net.IPNet{
155+
IP: net.ParseIP("172.16.0.0"),
156+
Mask: net.CIDRMask(12, 32),
157+
}
158+
if dockerBridge.Contains(ip) {
159+
return true
160+
}
161+
}
162+
163+
return false
164+
}
165+
166+
func getRealIP(r *http.Request) string {
167+
// Only trust proxy headers if request is from a trusted proxy
168+
if isTrustedProxy(r.RemoteAddr) {
169+
// X-Real-IP is set by nginx
170+
if ip := r.Header.Get("X-Real-IP"); ip != "" {
171+
return ip
172+
}
173+
174+
// X-Forwarded-For contains a list of IPs, take the first (original client)
175+
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
176+
ips := strings.Split(xff, ",")
177+
if len(ips) > 0 {
178+
return strings.TrimSpace(ips[0])
179+
}
117180
}
118181
}
119182

120-
ip = r.RemoteAddr
121-
if idx := strings.Index(ip, ":"); idx != -1 {
122-
ip = ip[:idx]
183+
// For direct connections or untrusted proxies, use RemoteAddr
184+
ip, _, err := net.SplitHostPort(r.RemoteAddr)
185+
if err != nil {
186+
// RemoteAddr might not have a port
187+
return r.RemoteAddr
123188
}
124189

125190
return ip

production/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ REDIRECT_URL_DEV="https://your-domain.com/auth/callback"
9090
SESSION_KEY="$(openssl rand -hex 32)"
9191
FRONTEND_ORIGIN_DEV="https://your-domain.com"
9292
CONTAINER_ORIGIN="http://syncserver:8080/"
93+
ENV="production"
94+
95+
# Rate limiting: Trusted proxies for correct client IP detection
96+
# Not needed if nginx runs on the same server (loopback is trusted by default)
97+
# Only set this if using an external load balancer:
98+
# TRUSTED_PROXIES="10.0.0.0/8,192.168.0.0/16"
9399
```
94100

95101
Create `.frontend.env` (see `example.frontend.env`):

0 commit comments

Comments
 (0)