Skip to content
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

feat: block out of bounds symlinks (#9738) #9843

Conversation

notfromstatefarm
Copy link
Contributor

@notfromstatefarm notfromstatefarm commented Jun 30, 2022

Signed-off-by: notfromstatefarm 86763948+notfromstatefarm@users.noreply.github.com

Checklist:

  • Either (a) I've created an enhancement proposal and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
  • The title of the PR states what changed and the related issues number (used for the release note).
  • I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
  • Does this PR require documentation updates?
  • I've updated documentation as required by this PR.
  • I have signed off all my commits as required by DCO
  • I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
  • My build is green (troubleshooting builds).

Fixes #9738

This PR implements @crenshaw-dev's idea of traversing the repository for symlinks and blocking further processing if any are found to traverse outside of the directory, even if the final target is within it. The check can be disabled via the repo server argument allow-oob-symlinks.

I believe this should cover all our bases since it is implemented inside runRepoOperation. It will print to the console whenever any are found.

I had to change many of the repository tests because they have the root of the mock repo being the root of the Argo repository. This meant that any time it 'fetched' the repository, it was finding the test files I created for the unit tests and thereby failing as it should.

Creating new app:
image

Refreshing an app that had bad symlinks added to it:
image

argocd-repo-server time="2022-07-01T03:52:31Z" level=warning msg="repository contains out-of-bounds symlink. repo: https://github.com/notfromstatefarm/argocd-symlink-test.git revision: f9c92205557f4b02e63268d1964e6a0acdb83310 file: badlinksinhere/badlink"

@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 5974f4f to 181c492 Compare June 30, 2022 21:43
@notfromstatefarm
Copy link
Contributor Author

notfromstatefarm commented Jun 30, 2022

Hah. It works SO good, it broke another test because it errored out complaining about the symlinks! Gotta figure out how to fix this..

@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 181c492 to 47da688 Compare June 30, 2022 22:03
@notfromstatefarm notfromstatefarm marked this pull request as draft July 1, 2022 00:07
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch 6 times, most recently from 4cbb79d to c32e586 Compare July 1, 2022 01:35
@notfromstatefarm notfromstatefarm marked this pull request as ready for review July 1, 2022 01:46
@codecov
Copy link

codecov bot commented Jul 1, 2022

Codecov Report

Merging #9843 (ae22f98) into master (71c1f54) will decrease coverage by 0.07%.
The diff coverage is 71.15%.

@@            Coverage Diff             @@
##           master    #9843      +/-   ##
==========================================
- Coverage   45.86%   45.78%   -0.08%     
==========================================
  Files         227      227              
  Lines       26862    27005     +143     
==========================================
+ Hits        12320    12364      +44     
- Misses      12862    12954      +92     
- Partials     1680     1687       +7     
Impacted Files Coverage Δ
util/app/path/path.go 66.66% <57.69%> (-17.95%) ⬇️
reposerver/repository/repository.go 64.30% <84.61%> (+0.51%) ⬆️
cmd/argocd-k8s-auth/commands/aws.go 17.64% <0.00%> (-0.36%) ⬇️
cmd/argocd/commands/admin/settings_rbac.go 23.80% <0.00%> (-0.26%) ⬇️
cmd/argocd/commands/admin/project.go 27.97% <0.00%> (-0.20%) ⬇️
cmd/argocd/commands/cluster.go 8.97% <0.00%> (-0.20%) ⬇️
cmd/argocd/commands/app.go 20.00% <0.00%> (-0.15%) ⬇️
cmd/argocd/commands/admin/app.go 30.37% <0.00%> (-0.13%) ⬇️
cmd/argocd/commands/app_resources.go 8.47% <0.00%> (-0.08%) ⬇️
server/server.go 53.18% <0.00%> (ø)
... and 21 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 71c1f54...ae22f98. Read the comment docs.

@notfromstatefarm notfromstatefarm marked this pull request as draft July 1, 2022 03:13
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch 4 times, most recently from d91a437 to 25ae38b Compare July 1, 2022 04:06
@notfromstatefarm notfromstatefarm marked this pull request as ready for review July 1, 2022 11:41
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 25ae38b to 99de448 Compare July 1, 2022 12:20
@crenshaw-dev
Copy link
Collaborator

You gotta quit putting up PRs so fast, I can't review them all! 😆 But for real, thanks for working on this.

Hah. It works SO good, it broke another test because it errored out complaining about the symlinks!

lol, I'm not surprised. Hopefully you were able to sort it.

A bunch of tests use relative paths, when the code really should only accept absolute paths.

@notfromstatefarm
Copy link
Contributor Author

You gotta quit putting up PRs so fast, I can't review them all! 😆 But for real, thanks for working on this.

Sorry! I saw a low-hanging fruit, had a slow day, and I love security. 😆

lol, I'm not surprised. Hopefully you were able to sort it.

Seems I've broken an E2E test.. Not entirely sure how since I'm not seeing my error. I have absolutely no idea how to dig into that yet but I'll figure it out. Is it OK if I DM you on the CNCF Slack every once in a while for assistance?

Copy link
Collaborator

@crenshaw-dev crenshaw-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome. First pass review finished. :-)

reposerver/repository/repository.go Outdated Show resolved Hide resolved
util/app/path/path.go Outdated Show resolved Hide resolved
util/app/path/path.go Outdated Show resolved Hide resolved
util/app/path/path.go Outdated Show resolved Hide resolved
util/app/path/path.go Outdated Show resolved Hide resolved
return nil
})
if found != "" && err.Error() == outOfBoundsError {
return true, found, nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a log.Warn? Out-of-bounds symlinks are suspicious, so it'd be nice to give admins a way to monitor them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log.Warn is in runRepoOperation 😄

Should I move it to the path function?

util/app/path/path.go Outdated Show resolved Hide resolved
@crenshaw-dev
Copy link
Collaborator

Is it OK if I DM you on the CNCF Slack every once in a while for assistance?

Sure! Generally I don't have time for support DMs, but I can always make time for contribution DMs. :-)

@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch 2 times, most recently from 87a41c2 to 59c6625 Compare July 1, 2022 14:08
@notfromstatefarm
Copy link
Contributor Author

Okay, refactored how the data is returned from the helper by using custom errors that carry the filename. Also added log fields and a repository integration test for out of bounds symlinks (instead of just out-of-bounds values files). Fixed the issue with files that contain ... Lets see if the E2E passes!

@crenshaw-dev
Copy link
Collaborator

Hm. Is there an e2e test with a symlink up to the base repo dir?

time="2022-07-01T14:46:38Z" level=error msg="`../../dist/argocd app create test-helm-repo --repo https://localhost:9444/argo-e2e/testdata.git/helm-repo/local --dest-server https://kubernetes.default.svc/ --helm-chart helm --project default --revision 1.0.0 --dest-namespace argocd-e2e--test-helm-repo-lltho --plaintext --server 127.0.0.1:8088 --auth-token *** --insecure` failed exit status 20: time=\"2022-07-01T14:46:38Z\" level=fatal msg=\"rpc error: code = InvalidArgument desc = application spec for test-helm-repo is invalid: InvalidSpecError: Unable to generate manifests in : rpc error: code = Unknown desc = open /tmp/_argocd-repo: permission denied\"" execID=b89db

util/app/path/path.go Outdated Show resolved Hide resolved
util/app/path/path.go Outdated Show resolved Hide resolved
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch 2 times, most recently from 24ccb83 to 3bf91c5 Compare July 2, 2022 16:23
@notfromstatefarm
Copy link
Contributor Author

notfromstatefarm commented Jul 2, 2022

Added another integration test into repository_test.go. Interestingly, I had to make a separate testdata2 directory for it - the other tests would fail as soon as it saw the out-of-bounds symlink in the 'helm chart root' aka testdata. This check is working too well. 😄

util/app/path/path.go Outdated Show resolved Hide resolved
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 3bf91c5 to 7655fb4 Compare July 2, 2022 19:29
@crenshaw-dev
Copy link
Collaborator

crenshaw-dev commented Jul 2, 2022

I'm considering whether we should move the goalposts just a little.

What if, along with preventing the symlink target from being outside the root dir, we prevented any part of the symlink path from ever leaving the root dir. This would prevent an attacker from being able to brute-force (or otherwise guess) the root dir path.

For example, consider this directory structure.

/tmp
  /_argocd-repo
    /{randomized dir name 1}
      secret
    /{randomized dir name 2}
      link (-> ../{randomized dir name 2}/../{randomized dir name 1})

I believe the current check would allow this symlink to pass, because its eventual destination does not lie outside the root dir. But the passing check allows the attacker to confirm that they've correctly guessed some external path. Then the attacker could start working on a different path traversal to get to the now-known external directory.

Granted, all the sensitive paths should be 128 bits of cryptographically secure entropy. But this would be an extra layer of protection in case of a particularly persistent adversary or a CSPRNG flaw.

I think the solution would involve splitting the link path and then reconstructing it, checking for an out-of-bounds error at each step. It would be all string manipulation operations (no filesystem work), so should be reasonably fast.

@crenshaw-dev
Copy link
Collaborator

But if we want to save that work for another time, that's understandable. 😆

@notfromstatefarm
Copy link
Contributor Author

notfromstatefarm commented Jul 2, 2022

I had to think about this one for a minute, but you're right! Even though the ultimate target can't be outside of the repository, the intermediate directories in the link must exist or it will fail, thus giving a way to determine if a path exists.

You're really good at thinking of these edge cases. 😄

I also seem to have introduced a flaky test.. Charts are switching spots. Bah.

@crenshaw-dev
Copy link
Collaborator

You're really good at thinking of these edge cases. 😄

Comes from pondering about 6 CVEs related to path traversal. 😆

I'll take a look at the flaky tests in a bit. Being lazy on the weekend so far and only thinking of ways to make things more difficult rather than more easy. 😉

@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 7655fb4 to 50da588 Compare July 2, 2022 20:21
@notfromstatefarm
Copy link
Contributor Author

notfromstatefarm commented Jul 2, 2022

I think this'll do the trick! It now traverses each step of the symlink to make sure it never leaves the bounds of the repo. Still gotta figure out that flaky test, though.

I added a unit test specifically to check this (../badlink3/test.txt) and it works!

@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch 3 times, most recently from 8601902 to 48b3c8d Compare July 2, 2022 20:42
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch 2 times, most recently from 6b09ef4 to 86caf5d Compare July 2, 2022 21:02
@notfromstatefarm
Copy link
Contributor Author

Okay, flaky test should be fixed. Ran it 100x locally. I think that's everything!

Copy link
Collaborator

@crenshaw-dev crenshaw-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to give this one last pass before smashing merge. But I'm 99.9% sure it's 100% awesome. :-)

Would you kindly add docs/operator-manual/upgrading/2.4-2.5.md and include a note about this change? Ideally the note would explain that we're trying to prevent 1) out-of-bounds symlinks that might be missed by other checks and 2) leaking any information about the presence or absence of files or directories outside the app's repo.

Also feel free to say "naw I'm busy," and I'll add the note later. :-)

@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 86caf5d to 005598f Compare July 5, 2022 15:47
Copy link
Collaborator

@crenshaw-dev crenshaw-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Down to optional nitpicks. I'm "go" for merge. :-) Going to look for someone to do a second review for good measure.

reposerver/repository/repository_test.go Show resolved Hide resolved
util/app/path/path.go Outdated Show resolved Hide resolved
util/app/path/path.go Show resolved Hide resolved
util/app/path/path_test.go Outdated Show resolved Hide resolved
Signed-off-by: notfromstatefarm <86763948+notfromstatefarm@users.noreply.github.com>
@notfromstatefarm notfromstatefarm force-pushed the feat/block-out-of-bounds-symlinks branch from 005598f to ae22f98 Compare July 5, 2022 16:57
@@ -318,6 +319,22 @@ func (s *Service) runRepoOperation(
return err
}
defer io.Close(closer)
if !s.initConstants.AllowOutOfBoundsSymlinks {
err := argopath.CheckOutOfBoundsSymlinks(chartPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just cosmetic change , i think it would be nice move this block to some dedicate function, so you will not need duplicate it fully in bottom

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna go ahead and merge this, and @notfromstatefarm can follow up with another PR if this can be DRY'ed out.

@crenshaw-dev crenshaw-dev merged commit 179d1e0 into argoproj:master Jul 11, 2022
@todaywasawesome todaywasawesome added this to the v2.5 milestone Jul 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Disallow all out-of-bounds symlinks in manifest repositories
4 participants