The error log and access log in your web root are a replay of every request, tokens included
A log file is a recording. The error log records what broke and where, file paths, stack traces, the query that failed. The access log records who came and what they asked for, the client IP, the method, and the full URL including everything in the query string. Both are meant for you, read locally, behind the server. Neither was written with the assumption that a stranger would download it.
When a log ends up in the web root, that assumption breaks, and the recording becomes a replay anyone can stream. GET /error.log or GET /logs/access.log and an outsider reads your traffic back: the internal paths your app uses, the absolute file locations on the server, the IP addresses of your visitors (which is personal data), and the tokens, session IDs, and reset codes that were passed in query strings and dutifully logged in full. A log file is one of the most underrated leaks because each line looks harmless, and the aggregate is a map plus a haul.
What each log gives up
The logs SurfaceCheckr looks for split into two kinds, and they leak different things.
Error logs, /error_log, /error.log, /logs/error.log, the Rails /log/production.log, are where the secrets hide. A stack trace prints the absolute path of your code on disk, which discloses your directory layout and framework. A failed database query can land in the log with its parameters. Apps log connection errors that include the host and sometimes the credentials. And the unglamorous truth is that developers log secrets by accident all the time, an API response dumped on error, a token printed "just to debug this once," a config value echoed at startup.
Access logs, /access.log, /logs/access.log, are where the tokens hide. The combined log format records the full request line, so any secret that traveled in a URL, a password-reset token, a session id in a query param, an API key a client appended, is sitting in the log in plaintext. The client IPs alongside them are PII you're now publishing. Even without a single secret, an access log is a complete inventory of every endpoint your app has, including the ones you never linked.
How a log lands in the docroot
Logs end up web-reachable through a handful of dull, repeatable mistakes.
The most common is logging configured to write into a path that happens to be under the web root, an app set to log to ./logs/ where . is the served directory, or a framework whose default log location overlaps the public folder. Another is a deploy that copies the whole project, log directory included, into a served location. A third is debugging: someone redirects output to error.log in the current directory to catch a problem, and the current directory is the docroot. The log keeps growing, the server keeps serving it, and the file accumulates months of requests anyone can read.
Logs go outside the web root, full stop
The fix is structural and simple: logs never live in a served directory, and the few that must stay nearby are blocked at the edge.
# app logs into ./logs under the web root # /logs/error.log and /logs/access.log are public URLs, # replaying internal paths, IPs, and logged secrets
Move logging to a path outside the document root, the system log directory or a sibling of the web root the server never serves, so there's no URL that maps to a log file. Add a deny rule for .log and /logs/ at the web server as a backstop in case one slips back. Then fix the upstream habit: scrub tokens out of logged URLs, and stop your app from writing secrets to the log on error, because a log that never contains a credential is a much smaller problem if it does leak. And rotate anything, a DB password, an API key, a reset token, that you find an exposed log already recorded.
This is catchable from outside because each log format has a fingerprint no normal page shares: the Apache/PHP error-log timestamp-and-severity bracket, the combined-access-log line with its IP, [dd/Mon/yyyy:HH:MM:SS +ZZZZ] stamp, and quoted request, the Rails logger's severity-and-PID prefix. SurfaceCheckr requests the common log paths and flags one only when that exact line structure comes back, so a soft-404 or an unrelated text file doesn't trip it. It reads the opening lines to confirm the format and stops; it never streams the whole log or harvests what's in it. For the application side of the same leak, an API that answers errors with stack traces and database errors reaches the same internals through a live response, and the small files that quietly map your server sit beside this one in the same pillar.
Read next
- terraform.tfstate, .aws/credentials, sftp.json: the infra files in your web rootThe files you forgot you deployed
- A FileZilla password isn't encrypted, it's base64. And it might be in your web root.The files you forgot you deployed
- The swap file, the SQLite db, and the Redis dump your app left in the web rootThe files you forgot you deployed
- Your Ansible inventory lists every server you run, and the passwords to log into themThe files you forgot you deployed
Find it before someone else does.
Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.