Skip to content

[API Proposal]: Add a [System.IO.Path] method to ensure that a relative path stays inside a directory #89785

@MatejKafka

Description

@MatejKafka

Background and motivation

See golang/go#56219 for a similar feature request for Go.

It is common for programs to accept filenames from untrusted sources. For example, an archive extractor might create files based on names in the archive, or a web server may serve the content of local files identified by a URL path. In both cases, the untrusted path is a relative path that should never leave a known directory specified in the program, e.g. if serving files from D:\webserver, paths like ../dir must not allow the user to read D:\dir.

Currently, user has to do this validation by hand, either by trying to parse the relative path directly (which is non-trivial and ripe with edge cases), or doing something like

var resolvedPath = Path.GetFullPath(Path.Combine(basePath, untrustedPath));
if (resolvedPath.StartsWith(basePath + Path.DirectorySeparatorChar)) { ... }

, of which I've also seen multiple incorrect variants on StackOverflow and similar sites (typically forgetting to add the directory separator to basePath). For either of these options, I'm not convinced that I can write a correct implementation without a lot of fuzzing.

API Proposal

Add a new method to System.IO.Path, bool IsPathLocal(string relativePath), which returns true if relativePath does not leave the current directory. The method should not be dependent on any particular directory, operating only on the string path, without referencing the filesystem.

API Usage

var untrustedPath = readClientRequest();
if (!Path.IsPathLocal(untrustedPath)) {
  sendError("can't touch that");
  return;
}
// ...
sendFileToClient(Path.Join(basePath, untrustedPath));

Alternative Designs

.. handling

I see two possibilities of how paths containing .. could be handled:

  1. Allow the path, as long as remains inside the directory (more complicated).
  2. Reject any path that contains .. as a segment. This is more restrictive, but I'd assume that in practice, it would work as well as the first variant for common scenarios while being much simpler to implement.

Retrieving the full path

Alternatively, the API could be implemented as a method string? JoinLocal(string basePath, string relativePath) (not sure about the name), which also receives the base directory and returns a string? which either contains the resolved absolute path, or is null if relativePath is not local. This might be more performant, since the API user will typically want to eventually resolve the path, but combines validation and resolution into a single API, which might be less flexible in cases where the API user wants to do the validation upfront, but only resolve the path later in the program.

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.IO

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions