File access

The feature that will read any file you name

Arbitrary file read is rated as an information disclosure and then triaged as medium. It is how you lose the server.

On 24 January 2024, Jenkins published an advisory for a flaw in how its controller handled command arguments. The command parser supported a convenience feature: an argument beginning with @ followed by a path was replaced with the contents of that file. It was on by default, it had been there for years, and it did what it said. Point it at a file on the server and the server read the file out.

It was rated CVSS 9.8 and it is in CISA's Known Exploited Vulnerabilities catalogue. What makes it worth writing about is not the severity but the shape: nobody implemented a file-reading endpoint, nobody wrote a download handler with a path parameter, and there was no traversal sequence to filter. A general-purpose argument-expansion feature simply turned out to be a file-read primitive, and the reach of that primitive was whatever the Jenkins process could open.

CVE
CVE-2024-23897 (Jenkins SECURITY-3314)
Product
Jenkins 2.441 and earlier, and LTS 2.426.2 and earlier. Fixed in 2.442 and LTS 2.426.3.
Class
Arbitrary file read (CWE-22 family — path traversal and unrestricted file access)
Severity
CVSS 9.8 (Critical)
Disclosed
24 January 2024 by the Jenkins project; exploited in the wild and added to CISA's Known Exploited Vulnerabilities catalogue
Impact
Attackers with Overall/Read permission could read entire files; without it, the first few lines. Because binary files holding Jenkins' cryptographic keys were readable too, the advisory describes paths from this to remote code execution.

The root cause, in one sentence

Something that arrived from a client was used to decide which file the server would open.

That sentence covers path traversal, local file inclusion, zip-slip, template injection by file path, and the Jenkins bug, which belongs to none of those categories neatly. The class is not defined by the ../ sequence everyone filters for — it is defined by a filename reaching a filesystem call with its origin forgotten. Filtering traversal sequences treats the symptom. The fix is that the client never names a file at all: it names an identifier, and the server resolves that identifier to a path it chose itself.

No payloads appear here. As in the other posts in this series, the mechanism is described to the depth a defender needs and no further. If you run Jenkins, work from the project's advisory rather than this page — and treat a controller that was exposed and unpatched as a controller whose keys and credentials are gone, because the whole point of this bug was reading the files that hold them.

The five shapes it takes

Every application we test has features in at least two of these categories. The question is never whether the application touches the filesystem; it is whether any of those paths can be steered.

1. Download, export and attachment endpoints

The obvious one, and still the most common. An endpoint takes a filename, a document reference, a report name or a storage key and returns the bytes. It is usually protected by a check that the value contains no traversal sequence — which is a blocklist, and therefore a bet that the developer thought of every encoding the filesystem and the runtime will accept between them. The version that holds is a database lookup: the client sends an opaque identifier, the row says which file that is, and the path never comes from the request.

2. Upload and archive extraction

The same bug with the arrow reversed, and worse, because writing beats reading. A filename supplied by the client decides where the upload lands; an uploaded archive contains entry names that decide where its members land. Archive extraction is the one teams miss most, because the dangerous value is inside the file rather than in the request — and a write that escapes the upload directory reaches configuration, cron directories, web roots and startup scripts. Resolve every destination path and confirm it sits inside the intended directory before writing a single byte.

3. Template, view and include resolution

Anywhere a request influences which template renders, which view is selected, which partial is included or which theme loads, the request is choosing a file. This also sits next to server-side template injection: if a client can name a template, the next question is whether it can supply one. Any parameter that looks like a page name, a layout, a locale file or a report format belongs on the list.

4. Converters, renderers and anything that follows a reference

PDF generators, image processors, document converters, spreadsheet importers, XML parsers and headless browsers all accept documents that may contain references to other resources — and a reference to a local path is a file read performed by your server on the attacker's behalf. This is where this class and SSRF meet: the same input decides what gets fetched, and file:// is a scheme like any other. We test converters as though the uploaded document were a request, because it is.

5. Expansion and indirection nobody filed under "file access"

This is the Jenkins shape, and the reason it is on this list. A configuration loader that follows a reference, a CLI or job runner that expands an argument, a logging format string, a bulk-import tool that accepts a path, a webhook handler that resolves an attachment locally, an "advanced" field in an admin screen. None of these were designed as file endpoints, so none were reviewed as file endpoints, and that is precisely why they survive. When we ask a development team to list the places their application reads a file from a path it was given, this category is never on the list they produce.

The reason we argue about the rating. An arbitrary file read gets triaged as "information disclosure, medium" far too often. But the files on a typical application server include the environment file, the framework's secret key, cloud credential files in the home directory, container service account tokens, private keys and the application's own source. A signing key is a path to forged sessions; a cloud credential is a path into the account; source code is a map to everything else. The Jenkins advisory makes the same point — this read was a route to code execution. We rate file read by what is reachable on your filesystem, and so does an attacker.

How we test for it

  1. Inventory every parameter that could name a resource. Not just values ending in .pdf. Report names, template and layout selectors, locale codes, storage keys, redirect and asset references, archive entry names, and any field whose value appears in a response as a filename. The list is always longer than the download endpoint everybody expects us to poke.
  2. Prove the direction of travel before anything else. We establish whether the parameter reaches a filesystem call at all, by comparing responses for a value that must exist against one that cannot. A difference in status, length or timing tells us there is a file lookup behind it — which is the finding worth pursuing, before any traversal is attempted.
  3. Test the encodings, not the sequence. A filter that catches one spelling of a traversal usually misses several others, because the runtime, the filesystem and any proxy in front each decode differently. We work through those systematically rather than trying one form and declaring the parameter safe.
  4. Test writes as carefully as reads, and with permission in writing. Upload filenames and archive entry names, checked against where files actually land. Write testing can damage a running system, so the targets and limits are agreed in the rules of engagement before we start, and we stay inside them.
  5. Feed the converters. Every document, image, spreadsheet and XML upload path gets a file whose content references a local resource, to see whether the server resolves it. Then we check whether what came back was rendered into the output — a converted document is an exfiltration channel.
  6. Establish what is reachable, then stop. Where a read is confirmed we determine its reach — which directories, whether the process can see secrets, and whether what is readable leads to authentication bypass or code execution — because that is the difference between a medium and a critical. We demonstrate the reach; we do not collect your secrets to prove it, and the limit is written down beforehand.

Each finding is written up with the request, the evidence, and the specific change that fixes it rather than a generic "validate input". That is the standard described in what's in a penetration test report.

Six things to check in your own application this week

  • Find every filesystem call in your codebase whose path argument derives from a request, and replace the path with an identifier your server resolves. This is the structural fix; everything else on this list is a safety net.
  • Where a path must be built, resolve it to its canonical absolute form and confirm it is inside the permitted directory after resolution. Checking the string before resolving it is the mistake.
  • Never trust a client-supplied filename as a storage name. Generate your own, and keep the original only as a display label.
  • Check your archive extraction for entry names that escape the destination, including symbolic links inside the archive. Validate every member's resolved destination before writing it.
  • Store uploads outside the web root, on a volume the application cannot execute from, and serve them through a handler rather than directly.
  • Assume one file read will happen, and audit what it would reach: your secret key, environment file, cloud credentials and service account tokens should not be readable by the application process if it never needs them. This is the control that caps the damage.

File access, access control and business logic are where the findings that matter actually live, and all three need someone reasoning about your application rather than matching it against a signature list. That is the gap our testing is built around — fixed price, agreed before we start, with the retest included.

Build your quote