master: CVE-2026-62268 fixes#9919
Merged
ThomasWaldmann merged 2 commits intoJul 20, 2026
Merged
Conversation
CVE-2026-62268 Fix 1/2 (port to master / borg 2)
A maliciously crafted archive could contain a symlink (e.g. "evil" -> "/etc")
followed by an item below it ("evil/passwd"). When extracting such an item, borg
followed the symlink in both the "remove existing file" step and the open()/mkdir()/
symlink() step, so it could delete or overwrite files outside the extraction directory.
Note: sounds bad, but no big issue in practice: to create such a malicious archive,
the attacker would need repository access and (for encrypted or authenticated repos)
also the borg key and passphrase.
borg create never produces such archives (it does not follow symlinks and stores
normalized relative paths), so extract_item() now verifies, before any destructive
operation, that the item's parent path contains no symlinked (or other non-directory)
component. Unsafe items are skipped with a warning and extraction continues. Verified-
safe parent directories are cached per extraction (Archive.safe_dirs) so each directory
is lstat'd at most once.
Also: skip redundant make_parent stat using the safe_dirs cache.
Differences from the 1.4-maint fix:
- Archived paths are split on "/" (borg 2 always uses "/", even on Windows), not os.sep.
- Symlink targets live in item.target (not item.source).
- The embedded-".." vector is already blocked in borg 2 one layer earlier: Item.path is
sanitized on encode (assert_sanitized_path) and decode (to_sanitized_path), so an item
whose path contains ".." can neither be stored nor read back. The "../" branch of
_check_safe_parent (BackupPathTraversalError) is therefore kept as defense-in-depth at
the destructive-operation boundary but is unreachable via a crafted archive; the test
asserts the item-layer rejection instead.
Adds BackupSymlinkParentError (mcode 108) and BackupPathTraversalError (mcode 109), and
regression tests for the symlinked-parent vector, the item-layer ".." rejection, and a
benign deep-tree extraction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CVE-2026-62268 Fix 2/2 (port to master / borg 2) The 1.4 fix validated the attacker-controlled hardlink source path (item.source) before using it as the os.link() target, and linked with follow_symlinks=False so a symlinked source component links the symlink itself rather than the external file it points to (an information disclosure, especially dangerous when restoring as root). borg 2 does not have the item.source vector: extraction hard links are driven by hlid, and the os.link() target (link_target) is never an attacker-controlled archived path - it is a path borg itself remembered (HardLinkManager, hlid -> path) from a previously extracted item, whose own path already passed Archive._check_safe_parent() (Fix 1/2). So there is nothing to validate there and borg 2 never raises BackupHardlinkSourceError; the class (and mcode 110) is still defined but kept only reserved for cross-branch parity with 1.4-maint. The remaining borg 2 vector is a hardlinked symlink: a crafted archive with a symlink "s" -> "/etc/shadow" plus a contentless hardlink "h" sharing s's hlid. Without care, os.link(s, h) follows the symlink and hard links the external file into the extracted tree. borg 2 already passed follow_symlinks=False here, but unconditionally - on a platform where os.link ignores follow_symlinks that raises NotImplementedError, which backup_io (OSError only) would not catch. Port the 1.4 supports_follow_symlinks guard so we fall back to a plain os.link() there, and document why follow_symlinks=False matters. Adds a regression test that crafts the hardlinked-symlink archive and asserts "h" is restored as a hard link to the symlink inode (faithful restore), not to the external victim file's inode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9919 +/- ##
==========================================
+ Coverage 85.27% 85.38% +0.11%
==========================================
Files 93 93
Lines 15932 16012 +80
Branches 2435 2444 +9
==========================================
+ Hits 13586 13672 +86
+ Misses 1636 1632 -4
+ Partials 710 708 -2 ☔ View full report in Codecov by Harness. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CVE-2026-62268 — port the extract security fixes to master (borg 2)
Ports the two 1.4-maint security fixes to master. Because borg 2's extract/hardlink architecture differs from 1.4, this is a re-implementation rather than a cherry-pick.
Upstream 1.4-maint commits:
3e9ed6d1ad6d3531b39c07b8ef3ecf41fce4437f— refuse unsafe parent paths141888f2fabcd57a300547c2aa63212cb213924d— refuse hardlinks with an unsafe source pathFix 1/2 — refuse unsafe parent paths (symlinked dir)
A crafted archive with a symlink (
evil→/etc) followed by an item below it (evil/passwd) madeextract_item()follow the symlink in both the "remove existing file" step and theopen()/mkdir()/symlink()step, so it could delete or overwrite files outside the extraction directory.Archive._check_safe_parent()verifies, before any destructive op, that an item's parent chain contains no..and no symlinked (non-directory) component. Unsafe items are skipped with a warning; verified-safe dirs are cached inArchive.safe_dirs(each dirlstat'd at most once), andmake_parentreuses that cache.BackupSymlinkParentError(mcode 108) andBackupPathTraversalError(mcode 109).Differences from 1.4:
/(borg 2 always uses/, even on Windows), notos.sep.item.target, notitem.source...vector is already closed in borg 2 one layer earlier:Item.pathis sanitized on encode (assert_sanitized_path) and decode (to_sanitized_path), so a..path can neither be stored nor read back. The..branch of_check_safe_parentis therefore defense-in-depth only; the test asserts the item-layer rejection instead.Fix 2/2 — harden hardlinked-symlink restore
The 1.4 fix validated the attacker-controlled
item.sourceused as theos.link()target and linked withfollow_symlinks=False.borg 2 has no
item.sourcevector: extraction hardlinks are driven byhlid, and theos.link()target is always a path borg itself remembered (HardLinkManager,hlid → path) from a previously extracted item whose path already passed_check_safe_parent(). So there is nothing to validate and nothing raisesBackupHardlinkSourceError— the class (mcode 110) is defined only to reserve the number for cross-branch parity with 1.4-maint.The remaining borg 2 vector is a hardlinked symlink (symlink
s→/etc/shadowplus a contentless hardlinkhsharings's hlid). borg 2 already passedfollow_symlinks=False, but unconditionally — on a platform whereos.linkignores it, that raisesNotImplementedError, whichbackup_io(OSError only) would not catch. This ports the 1.4os.supports_follow_symlinksguard (fall back to a plainos.link()), plus a comment on whyfollow_symlinks=Falseis security-relevant.Tests
test_extract_through_symlinked_parent— symlinked-parent vector is refused; out-of-tree victim untouched; symlink restored faithfully.test_extract_path_with_embedded_dotdot_rejected_at_item_layer— documents that borg 2 rejects..paths at the item layer.test_extract_deep_tree_ok— the guard /safe_dirscache doesn't break normal deep-tree extraction.test_extract_hardlinked_symlink_does_not_leak_target—his restored as a hardlink to the symlink inode, not to the external victim's inode. Verified to fail iffollow_symlinks=Falseis removed.All new tests pass; full local
extract_cmd_test.pyrun: 33 passed, 5 skipped.blackclean.🤖 Generated with Claude Code