GHSA-56x6-q882-mf27

Suggest an improvement
Source
https://github.com/advisories/GHSA-56x6-q882-mf27
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-56x6-q882-mf27/GHSA-56x6-q882-mf27.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-56x6-q882-mf27
Aliases
Published
2026-11-09T21:18:29Z
Modified
2026-11-09T21:30:07Z
Severity
  • 6.5 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L CVSS Calculator
Summary
Remark42: Rate limit bypass via unconditional trust of X-Forwarded-For/X-Real-IP headers in direct-internet deployment
Details

Summary

Remark42 applied chi's middleware.RealIP globally, trusting True-Client-IP / X-Real-IP / X-Forwarded-For headers without verifying the request came from a trusted proxy. The per-IP rate limiters key on the overwritten r.RemoteAddr, so any client can bypass every per-IP rate limiter by rotating spoofed IP headers. The same spoofed IP also drives other per-IP controls (stored comment IP, anonymous identity derivation, and --votes-ip vote de-duplication). This affects deployments reachable directly from the internet (Remark42's built-in HTTP, static TLS, and ACME modes are all documented and supported), as well as any proxied deployment where the proxy forwards an attacker-controlled recognised header.

Details

The router applies middleware.RealIP globally, ahead of the per-route limiters, without any check that the server is behind a trusted proxy (in v1.16.1, backend/app/rest/api/rest.go:207).

The rateLimiter function (v1.16.1, backend/app/rest/api/rest.go:769-779) builds a tollbooth limiter with IPLookup{Name: "RemoteAddr"}, which reads r.RemoteAddr. Since chi's middleware.RealIP (v1.16.1, backend/vendor/github.com/go-chi/chi/v5/middleware/realip.go:42-55) unconditionally overwrites r.RemoteAddr from True-Client-IP > X-Real-IP > first X-Forwarded-For (accepting any syntactically valid IP), any client can set its apparent IP to an arbitrary value.

This bypasses every per-IP rate limiter that runs after RealIP: /auth (2/s), avatars (100/s), the image proxy (10/s), open / protected / admin route groups (10/s), root routes (50/s), and /web/* (20/s). Comment and vote creation go through the update limiter, whose server default is UPDATE_LIMIT=0.5/s (burst 1), not 10/s. The global 1,000 concurrent-request throttle still applies, so this is a per-IP-limit bypass rather than truly unlimited throughput. chi's own documentation warns that middleware.RealIP should be used only behind a trusted proxy.

PoC

# Deploy Remark42 reachable directly from the internet (built-in HTTP / TLS / ACME, no proxy that overwrites the headers).
# Comment creation is limited to UPDATE_LIMIT (default 0.5/s, burst 1) per IP.

# Rotate X-Real-IP across many requests; each distinct value gets its own token bucket.
for i in $(seq 1 100); do
  curl -s -o /dev/null -w '%{http_code}\n' \
    -H "X-Real-IP: 192.0.2.$i" \
    -X POST 'https://remark42.example.com/api/v1/comment?site=site1' \
    -H 'Authorization: Bearer YOUR_JWT' \
    -H 'Content-Type: application/json' \
    -d '{"text":"spam","locator":{"site":"site1","url":"https://example.com/post"}}'
done | sort | uniq -c
# With rotating headers the responses stay 201; without them, requests beyond the burst return 429.

Impact

By rotating the spoofed client IP an attacker bypasses the per-IP token buckets on all rate-limited endpoints and defeats other per-IP controls, enabling: (a) spam comment creation beyond the configured per-IP limit, (b) confirmation-email / notification flooding to supplied addresses, (c) high-volume outbound image fetching and resource consumption via the image proxy (its transport still blocks private/reserved destinations and DNS rebinding, so this is amplification, not new SSRF reach), (d) authentication-endpoint abuse, (e) where --votes-ip is enabled, defeating per-IP vote de-duplication (ballot stuffing), and (f) minting fresh anonymous identities and poisoning the stored comment IP and request logs, since the anonymous user ID is derived from name + RemoteAddr. The global concurrency ceiling and per-request timeouts remain, so availability impact is limited.

Resolution

Mitigation support was added in v1.16.2 (umputun/remark42#2116, follow-up in #2118). Upgrading alone does not remediate the issue: --trusted-proxy must be configured.

The fix adds --trusted-proxy / TRUSTED_PROXY. When set, the forwarding headers (X-Real-IP, then CF-Connecting-IP, then the leftmost public X-Forwarded-For) are honoured only when the direct TCP peer falls inside a trusted-proxy network; for a request from any other peer those headers are dropped and the real socket IP is used. When it is unset, the previous trust-all behaviour is preserved for backward compatibility and a startup warning is logged, so an un-configured v1.16.2 remains exploitable. True-Client-IP is no longer recognised. TRUSTED_PROXY takes a comma-separated list; on the command line, repeat --trusted-proxy per entry.

Required configuration:

  • Reachable directly from the internet: set a value that no real client matches so the forwarding headers are always ignored, e.g. --trusted-proxy=127.0.0.1/32 (only when Remark42 is not actually reached over loopback).
  • Behind a reverse proxy: point --trusted-proxy at the proxy's network only, block direct access to Remark42, and make the proxy overwrite X-Real-IP with the real client and discard client-supplied forwarding headers. A proxy that merely appends to X-Forwarded-For, or that forwards a client-supplied X-Real-IP, still lets the client choose the reported IP. Note that the shipped nginx manual sets X-Real-IP but does not strip a client True-Client-IP.
  • Before v1.16.2: strip True-Client-IP, X-Real-IP, and X-Forwarded-For at the edge and set a known-safe client-IP header.

See the Trusted proxies and client IP docs.

Database specific
{
    "cwe_ids": [
        "CWE-348",
        "CWE-807"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-11-09T21:18:29Z",
    "nvd_published_at": "2026-11-05T21:18:53Z",
    "severity": "MODERATE"
}
References

Affected packages

Go / github.com/umputun/remark42/backend

Package

Name
github.com/umputun/remark42/backend
View open source insights on deps.dev
Purl
pkg:golang/github.com/umputun/remark42/backend

Affected ranges

Type
SEMVER
Events
Introduced
1.0.0
Fixed
1.16.2

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-56x6-q882-mf27/GHSA-56x6-q882-mf27.json"

Go / github.com/umputun/remark42

Package

Name
github.com/umputun/remark42
View open source insights on deps.dev
Purl
pkg:golang/github.com/umputun/remark42

Affected ranges

Type
SEMVER
Events
Introduced
1.0.0
Last Affected
1.2.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-56x6-q882-mf27/GHSA-56x6-q882-mf27.json"