Files
nginx-ui/api
0xJacky 7d33c62082 fix(api): stop sending filesystem paths raw in query strings
Managed WAF rulesets read a path in a query string as a directory-traversal
attempt and block the request at the edge. Measured against Cloudflare:

    GET /api/nginx_log/preflight                          -> 403 application/json  (our auth)
    GET /api/nginx_log/preflight?log_path=%2Fvar%2Flog%2Fnginx%2Faccess.log
                                                          -> 403 text/html         (Cloudflare)

Same endpoint, same credentials; only the path-shaped value differs. Note the
blocked request was already percent-encoded — WAFs normalise that before
matching, so escaping harder does not help. This hits any operator behind a
WAF, so it belongs in the product rather than in a per-zone exception.

Send such values base64url-encoded behind a `b64_` prefix instead. The output
alphabet is [A-Za-z0-9_-], leaving no slash, no dot and nothing for a signature
to match.

Seven parameters were affected, two worse than the one that was reported:
`filepath` on GET /api/config_histories is always a fully absolute path, and
`path` on GET /api/nginx_logs is a search box whose contents the operator types
straight into the URL.

Two constraints shaped the implementation:

Decoding happens in each handler, strictly BEFORE path validation. The other
order would let `../../../etc/passwd` hide inside the encoding and slip past
IsUnderDirectory and IsValidLogPath. Tests in internal/config and
internal/nginx_log/utils pin the ordering down so it cannot be reversed later.
It cannot live in middleware either: AuthRequired reads c.Query("x_node_id"),
which freezes gin's query cache before any later middleware could rewrite it.

The frontend does it in the one existing request interceptor rather than at
call sites. That is not merely tidier — two of the seven parameters are
generated by useCurdApi and have no call site to patch. The transform is an
allowlist keyed on URL, because GET /api/settings/protected also takes `path`
and its value is a settings key like `app.jwt_secret` that must arrive intact.

Raw values keep working: a value is treated as encoded only if it starts with
`b64_`, decodes as base64url, and yields valid NUL-free UTF-8. Absolute paths
fail the first clause, relative config paths contain characters outside the
alphabet and fail the second. A bare "try to decode, else treat as raw"
heuristic was rejected — `defaultsite` is itself valid base64url and would
silently decode to binary.

Left alone deliberately: paths already travelling in POST bodies, the SPA's own
routes (hash history keeps them out of the request line), and
/api/settings/protected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 11:18:09 +08:00
..