Manually resolve path relatively to root_dir to prevent escape#328
Manually resolve path relatively to root_dir to prevent escape#328HorlogeSkynet wants to merge 2 commits into
root_dir to prevent escape#328Conversation
509cc4f to
5ab542e
Compare
There was a problem hiding this comment.
Hi @HorlogeSkynet , maybe I'm misunderstanding something, but right now I only see the part that prevents "unjust" escapes from the chroot (from absolute links or from going above the root) but I don't see any code that would "bend" "legit" absolute symlinks back into the chroot.
Let me demo what I think we'd need in some form:
def resolve_chroot_symlink(link_location, root_dir):
assert os.path.commonprefix([link_location, root_dir]) == root_dir
while True:
try:
resolved = os.readlink(link_location)
except FileNotFoundError: # includes case "not a symlink"
return link_location
if resolved.startswith('/'):
# i.e. absolute path (with regard to the chroot)
# that we need to move back inside the chroot
resolved = os.path.join(root_dir, resolved.lstrip('/'))
else:
# i.e. relative path that we make absolute
resolved = os.path.join(os.path.dirname(link_location), resolved)
if os.path.commonprefix([resolved, root_dir]) != root_dir:
raise Value('ESCAPE DETECTED') # TODO
link_location = resolvedWhat do you think?
5ab542e to
b9004e8
Compare
|
Dear Sebastian, sorry for the delay. So I tweaked your function a bit in order to :
You will see that I opted for links resolution on the verge of calling I've implemented three short test cases to check our new logic (note : relative symbolic links resolving is already tested by regular rootfs under I also took the liberty to update our Bye ! Thanks for your time 👋 |
root_dir, and prevent escape
hartwork
left a comment
There was a problem hiding this comment.
Hi @HorlogeSkynet kudos for the new tests and the loop detection! 👍 I believe a few details need more work, please see my comments below
b9004e8 to
c97754d
Compare
🙏 My mistake ! There was an empty directory that Git didn't track, I've just fixed this 😉 Waiting for your inputs, thanks for your time 👋 EDIT : If you are tired of these round trips, feel free to directly amend the branch ! |
c97754d to
5186ad6
Compare
5186ad6 to
568cce4
Compare
568cce4 to
1087c2d
Compare
|
PR's (finally) ready @hartwork ! |
There was a problem hiding this comment.
Hello @HorlogeSkynet, sorry for the big delay. I found some remaining issues. Please see my four comments below for details:
a1af721 to
91de73f
Compare
5d9b5ba to
e40187b
Compare
7dc0126 to
a46f5a7
Compare
af1c8ad to
7a85cfa
Compare
|
Hey @hartwork, and thanks for your answer. Indeed, I completely forgot about this (these) discussions, since GitHub archived them... I took a quick glimpse of your branch which I find very complex (but this bug/feature is not trivial at all, and my new algorithm is possibly sill broken). So I'll sum up the situation here again, so @python-distro/maintainers will have the full context to properly review this (these) branches : The idea is to "fix" the
So we (@hartwork and I) worked on these issues without opting for solutions as requiring system privileges or using a third-party dependency.
From here, we see multiple options that I'd like to propose/discuss with you :
Thanks for your time, see you all 👋 |
7a85cfa to
dc19990
Compare
Implementation reworked and rebased
dc19990 to
5e7a64f
Compare
root_dir, and prevent escaperoot_dir to prevent escape
1284169 to
f0e57c0
Compare
f0e57c0 to
9f3ae66
Compare
|
@adamjstewart As #369 is now closed and we eventually don't "open" another file, I take the liberty to re-propose this patch to address concerns raised in #311. I agree it's a big piece, but as it's safely guarded by |
9f3ae66 to
50f1498
Compare
|
@HorlogeSkynet this one's a bit outside my area of expertise. I think someone else would be better to review this, as I barely understand the problem it's trying to solve. |
Two years and a half, except for Sebastian, no one really gave a damn about this issue. This PR resolves rootfs symbolic links relatively to chroot to prevent this behavior, but is still vulnerable to TOCTOU, |
|
Hello @nir0s, could we have your review/input on this ? I'd like to incorporate this (security) fix in v1.10 release. |
|
^ @nir0s if you're back online... |
50f1498 to
31babb6
Compare
|
Will take a look promptly. |
|
|
||
| return root_path / Path(rel_path) | ||
|
|
||
| def _resolve_path_relatively_to_chroot(self, path: str) -> Path: |
There was a problem hiding this comment.
The logic here is correct as far as I can tell, but it's hard to audit for security-sensitive code: ~50 lines of relative_parts[:i] slicing, mutate-and-restart, for/else, and a lexical relative_to. I'd prefer a single-pass input-pop / output-push segment stack (mirroring os.path.realpath(strict=False) with a clamped root) — it's linear and much easier to reason about. Worth considering folding that style in here.
There was a problem hiding this comment.
Not critical, but would be nice.
There was a problem hiding this comment.
I'd prefer not getting back to work on this matter to rewrite one more time this algorithm... But feel free to change implementation with your idea and I'd be pleased to review it.
| self.usr_lib_dir, _OS_RELEASE_BASENAME | ||
| ) | ||
|
|
||
| def __isfile(path: str) -> bool: |
There was a problem hiding this comment.
This resolves + stats the path, and then _os_release_info resolves + opens the same path again - two walks plus a stat→open TOCTOU window. The distro-release path uses "open and catch" instead; doing the same here would drop both the double resolution and the race.
There was a problem hiding this comment.
If we (only) delegate this case handling to _os_release_info, we cannot accomplish the "backward-compatible way of setting self.os_release_file attribute (added in #262). We need to know whether the path actually resolve to a file or not, to properly address /etc or /usr/lib variant.
Or I missed your point here.
Explicitly disabling third-programs data sources is not required since 2a89f76 (disabled by default if `root_dir` is set).
31babb6 to
d87e20f
Compare
d87e20f to
dabff05
Compare
This patch is a followup of #311 (2a89f76). It appeared that we were not resolving paths when reading from files. This means that symbolic links present under `root_dir` could be blindly followed, eventually leading _outside_ of `root_dir` (i.e host own files). Note : this patch **only** changes LinuxDistribution `root_dir` parameter behavior.
dabff05 to
17c7799
Compare
Dear Sebastian, dear maintainers,
This PR drafts a first implementation to address concerns raised in #311 review.
Currently, paths are not manually resolved, and by following symbolic links, this may lead to outside specified
root_dir.By using
os.path.realpathandos.path.commonprefix, we can verify that important paths are still prefixed byroot_dir, once they have been resolved.It turned out enforcing those checks was not as easy as I thought :
PermissionErroractually fits, but I was out of option__prevent_root_dir_escapefunction might require a renaming tooAt least, the new functional test actually enlightens the issue.
--> A deep review is required, ideas are welcome and commits on this branch too 🙃
Have a nice WE all 🙇