Recently there was an LWN news item pointing to an article on the Go blog about Traversal-resistant file APIs.
That’s a nice API. I’d also be nicer in Julia since we could use a do
block instead of defer
.
My two cents here: if Paths API has some kind of abstract interface, then maybe it can be used consistently across packages like Tar.jl, ZipArchives.jl, ZipFile.jl (maybe with some constraints)?
Thanks for the link! Incidentally this touches on one of the tensions in the current design: my desire to break with select conventions in the name of consistency/safety.
For instance, currently p"/some/absolute/path" * p"../../../../../../../../etc/passwd"
will raise an error once we try to get the parent directory of p"/"
, however the “standard” behavior is for the parent of /
to be /
.
I particularly enjoyed reading this section:
If the attacker controls part of the local filesystem, they may be able to use symbolic links to cause a program to access the wrong file:
// Attacker links /home/user/.config to /home/otheruser/.config: err := os.WriteFile("/home/user/.config/foo", config, 0o666)
If the program defends against symlink traversal by first verifying that the intended file does not contain any symlinks, it may still be vulnerable to time-of-check/time-of-use (TOCTOU) races, where the attacker creates a symlink after the program’s check:
Since this is exactly the issue that is systematically eliminated by requiring the use of a FD-as-Path type, as I am now experimenting with (and based on encouraging results, advocating for).
I don’t think it would be too hard to extend the proposal slightly and have rooted-ness be an extra flag that the FD-as-Path type can have, which is then automatically propagated to all path joining operations.
From reading that article, with this proposal will give us a better story with this class of vulnerabilities than Go
RAII would be nice here, but in the current experiment I’m just using a finaliser. I’m not sure that do
is really what we want, since it’s good if the same handle is passed around more instead of being dropped and re-acquired.
Sure, why not How does julia-basic-paths/abstractpaths.jl at main - tec/julia-basic-paths - Code by TEC look to you?
It’s slightly imprecise, but I’m thinking that maybe PathHandle
is the name to go with here. I think having “handle” in the name is important, and while FSHandle
etc. are most accurate, by putting Path
in the name we get an explicit connection to the Path
type and an indication of the relationship between the two. Appearing as a completion with Path<tab>
is a nice bonus. I think together this more than makes up for the loss in accuracy.
So, with this, there are three kinds of system-native paths:
Path
as a “pure” path for the current system (an alias ofPosixPath
orWindowsPath
)PathHandle
for a file descriptor/filesystem handle produced from aPath
DirEntry
is a parent directoryPathHandle
combined with a relativePath
and (optional) metadata.
All of these types can be converted between (e.g. PathHandle
to a Path
, DirEntry
to a PathHandle
, etc.) using the platform filesystem APIs. We can do this automatically where convenient, and require a particular type when we want to prod the user into behaving more safely (e.g. requiring them to acquire a handle, to encourage them to re-use it).