What Gixy reports
[try_files_is_evil_too] The try_files directive is evil without open_file_cache
Severity: MEDIUM
Reason: The try_files directive introduces performance overhead
without open_file_cache
This is a performance finding, not a security one. It is rated MEDIUM because the cost is real and continuous, not because anything is exploitable.
What it costs
Take the canonical front-controller pattern:
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
For a request to /some/page, nginx checks /some/page, then
/some/page/, then falls back. That is up to three filesystem lookups, each an
open() or stat() syscall, before any content is served. On a
single-page application where nearly everything falls through to the fallback, you pay the
full set every time.
None of it is cached. nginx asks the kernel again on the next request, and the one after that. The page cache makes each individual call cheap, so this rarely shows up as a problem on a laptop -- and reliably does on network storage, on a container with a layered filesystem, or at a few thousand requests per second where the syscall count itself becomes the bottleneck.
The fix
Enable open_file_cache. nginx then remembers file descriptors, sizes, modification times, and lookup errors:
http {
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
server {
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
}
}
What each one does:
max-- how many entries to hold; evicted least-recently-used. 10000 is comfortable for most sites.inactive-- drop entries not used within this window.open_file_cache_valid-- how often to re-check that a cached entry is still accurate.open_file_cache_min_uses-- only cache after this many uses, so one-off requests do not evict hot entries.-
open_file_cache_errors on-- cache the misses too. This is the one that matters most fortry_files, because the expensive part of a fallback chain is the failed lookups.
Deploys need care
With open_file_cache_valid 60s;, nginx can serve a cached descriptor for up
to a minute after you replace a file. For atomic deploys that swap a symlinked release
directory this is usually invisible, since the new path is a cache miss. For in-place
file replacement, either lower the validity window or reload nginx after deploying.
Verify the fix
gixy /etc/nginx/nginx.conf
nginx -t && nginx -s reload
# Count the syscalls before and after, on a single worker:
strace -c -f -p $(pgrep -f 'nginx: worker' | head -1) &
ab -n 1000 -c 10 https://example.com/some/page
# Compare the "open" and "stat" rows in the summary.
On Linux, perf stat -e syscalls:sys_enter_openat is a lighter-weight way to see
the same thing without strace's overhead distorting the measurement.
When this is a false positive
-
Low-traffic servers. At a few requests per second the syscalls are
irrelevant and
open_file_cacheadds a deploy-staleness consideration for no measurable gain. Reasonable to leave. - Content that changes constantly. If files are rewritten faster than the validity window, caching lookups works against you.
-
A pure-proxy location. A
try_filesthat only ever falls through to a named location touching no real files has little to cache -- although the failed lookups still cost, which is whatopen_file_cache_errorsaddresses.
Read this one as "you are paying for something you have not tuned" rather than "you have a bug". Background on the pattern: try_files is evil, too.
Reference
- try_files_is_evil_too in the Gixy documentation — what the check inspects and its options
- All Gixy checks — the full list by severity
- Plugin source — the exact detection logic