Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MatejKafka opened this issue Aug 1, 2023 · 6 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.IO

Comments

@MatejKafka
Copy link

MatejKafka commented Aug 1, 2023

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

@MatejKafka MatejKafka added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Aug 1, 2023
@ghost ghost added area-System.IO untriaged New issue has not been triaged by the area owner labels Aug 1, 2023
@ghost
Copy link

ghost commented Aug 1, 2023

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

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;
}
// use `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? CombineLocal(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

Author: MatejKafka
Assignees: -
Labels:

api-suggestion, area-System.IO, untriaged

Milestone: -

@Tornhoof
Copy link
Contributor

Tornhoof commented Aug 1, 2023

Before you try working with startswith, take a look at https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=net-7.0#system-io-path-getrelativepath(system-string-system-string)

and then check if the return of that method contains .. or equals the second arg.

That method also takes care of Path.GetFullPath for you.

@Clockwork-Muse
Copy link
Contributor

From a usefulness standpoint, something that only checks if a path is a subdirectory of working directory seems overly restrictive. And potentially its own security hole, because the working directory might not be what you expect, anyways (as opposed to the source directory of the application).

I personally would prefer some sort of IsSubdirectoryOf(...)-type method (although resolution in some edge cases might be fun), if we're going that route.

@danmoseley
Copy link
Member

I'm not sure that any examination of strings can provide a bulletproof guarantee, given variations in OS and file system behaviors, hard links, and so forth. This is something code access security (CAS) tried to do. The only sure way is likely to have the OS tell you, somehow.
Cc @JeremyKuhne for thoughts

@MatejKafka
Copy link
Author

MatejKafka commented Aug 2, 2023

@Clockwork-Muse

only checks if a path is a subdirectory of working directory

The proposed API is not dependent on the current working directory. If the IsPathLocal method returns true, it should be safe to combine the path with any base path you want (Path.Join(basePath, untrustedPath)).


@danmoseley I'm also not sure about that, but as long as symlinks are not involved, I haven't found any path where interaction with the filesystem is necessary to determine whether it's safe. If such paths exist, it might be necessary to use a different implementation for each platform (which is consistent with what other methods in System.IO.Path do).

@JeremyKuhne
Copy link
Member

This is a dupe of #87581 I think. I gave some commentary there on design of such an API: #87581 (comment)

@jozkee jozkee closed this as not planned Won't fix, can't repro, duplicate, stale Aug 3, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Aug 3, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Sep 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.IO
Projects
None yet
Development

No branches or pull requests

6 participants