Validate archive entries and stage inside the pack root
- Reject archive members that are absolute, escape via .., are special files, or link outside the destination, before anything is written - Stage under the pack root so the swap is a same-filesystem rename, with an EXDEV copy fallback; propagate registry auth errors and validate dir_name
This commit is contained in:
@@ -63,6 +63,85 @@ describe('swap', function()
|
||||
assert_match('cannot move new install into place', err)
|
||||
assert_eq({ 'old' }, vim.fn.readfile(root .. '/target/marker'))
|
||||
end)
|
||||
|
||||
-- Forced EXDEV: deterministic everywhere, no second mount required.
|
||||
local function with_exdev_on(path, fn)
|
||||
local uv = vim.uv or vim.loop
|
||||
local real = uv.fs_rename
|
||||
uv.fs_rename = function(from, to)
|
||||
if from == path then
|
||||
return nil, 'EXDEV: cross-device link not permitted: ' .. from .. ' -> ' .. to, 'EXDEV'
|
||||
end
|
||||
return real(from, to)
|
||||
end
|
||||
local ok, err = pcall(fn)
|
||||
uv.fs_rename = real
|
||||
if not ok then
|
||||
error(err, 0)
|
||||
end
|
||||
end
|
||||
|
||||
it('falls back to a copy when staged and target are on different filesystems', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/target', 'p')
|
||||
vim.fn.writefile({ 'old' }, root .. '/target/marker')
|
||||
vim.fn.mkdir(root .. '/staged/lua', 'p')
|
||||
vim.fn.writefile({ 'new' }, root .. '/staged/lua/plug.lua')
|
||||
|
||||
with_exdev_on(root .. '/staged', function()
|
||||
assert_true(fsmod.swap(root .. '/staged', root .. '/target'))
|
||||
end)
|
||||
assert_eq({ 'new' }, vim.fn.readfile(root .. '/target/lua/plug.lua'))
|
||||
assert_true(not fsmod.exists(root .. '/target/marker'))
|
||||
assert_true(not fsmod.exists(root .. '/target.vimpack-old'))
|
||||
assert_true(not fsmod.exists(root .. '/target.vimpack-new'))
|
||||
assert_true(not fsmod.exists(root .. '/staged'))
|
||||
end)
|
||||
|
||||
it('restores the previous install when the cross-filesystem copy fails', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/target', 'p')
|
||||
vim.fn.writefile({ 'old' }, root .. '/target/marker')
|
||||
|
||||
local ok, err
|
||||
with_exdev_on(root .. '/absent', function()
|
||||
ok, err = fsmod.swap(root .. '/absent', root .. '/target')
|
||||
end)
|
||||
assert_true(not ok)
|
||||
assert_match('cannot move new install into place', err)
|
||||
assert_eq({ 'old' }, vim.fn.readfile(root .. '/target/marker'))
|
||||
assert_true(not fsmod.exists(root .. '/target.vimpack-new'))
|
||||
end)
|
||||
|
||||
it('copies a tree across a real filesystem boundary', function()
|
||||
local root = tmpdir()
|
||||
local other = otherfs_dir(root)
|
||||
if not other then
|
||||
return
|
||||
end
|
||||
vim.fn.mkdir(root .. '/staged/lua', 'p')
|
||||
vim.fn.writefile({ 'new' }, root .. '/staged/lua/plug.lua')
|
||||
vim.fn.mkdir(root .. '/staged/empty', 'p')
|
||||
|
||||
assert_true(fsmod.copy_tree(root .. '/staged', other .. '/copy'))
|
||||
assert_eq({ 'new' }, vim.fn.readfile(other .. '/copy/lua/plug.lua'))
|
||||
assert_true(fsmod.exists(other .. '/copy/empty'))
|
||||
fsmod.rm_rf(other)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('safe_relative', function()
|
||||
it('accepts a plain relative path', function()
|
||||
assert_eq('lua/plug.lua', fsmod.safe_relative('./lua/plug.lua'))
|
||||
assert_eq('lua', fsmod.safe_relative('lua/sub/..'))
|
||||
end)
|
||||
|
||||
it('rejects absolute paths and escapes', function()
|
||||
assert_nil(fsmod.safe_relative('/etc/passwd'))
|
||||
assert_nil(fsmod.safe_relative('../evil'))
|
||||
assert_nil(fsmod.safe_relative('a/../../evil'))
|
||||
assert_nil(fsmod.safe_relative(''))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('extract', function()
|
||||
@@ -102,4 +181,91 @@ describe('extract', function()
|
||||
assert_true(not result.ok)
|
||||
assert_match('extraction failed', result.err)
|
||||
end)
|
||||
|
||||
it('unpacks a zip', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
|
||||
vim.fn.writefile({ 'return {}' }, root .. '/src/plug/lua/plug.lua')
|
||||
vim.fn.system({ 'sh', '-c', 'cd ' .. vim.fn.shellescape(root .. '/src') .. ' && zip -qr ../plug.zip plug' })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local result = wait_extract(root .. '/plug.zip', root .. '/dest')
|
||||
assert_true(result.ok, tostring(result.err))
|
||||
assert_true(fsmod.exists(root .. '/dest/plug/lua/plug.lua'))
|
||||
end)
|
||||
|
||||
it('refuses a tarball that climbs out of the destination', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/src/plug', 'p')
|
||||
vim.fn.writefile({ 'pwned' }, root .. '/src/plug/payload')
|
||||
vim.fn.system({
|
||||
'tar',
|
||||
'-czf',
|
||||
root .. '/evil.tar.gz',
|
||||
'-C',
|
||||
root .. '/src',
|
||||
'--transform',
|
||||
's|^plug|../evil|',
|
||||
'plug/payload',
|
||||
})
|
||||
|
||||
local result = wait_extract(root .. '/evil.tar.gz', root .. '/dest')
|
||||
assert_true(not result.ok)
|
||||
assert_match('refusing to extract', result.err)
|
||||
assert_match('../evil/payload', result.err)
|
||||
assert_true(not fsmod.exists(root .. '/evil'))
|
||||
assert_true(not fsmod.exists(root .. '/dest/payload'))
|
||||
end)
|
||||
|
||||
it('refuses a tarball holding an absolute path', function()
|
||||
local root = tmpdir()
|
||||
local victim = root .. '/abs-evil'
|
||||
vim.fn.writefile({ 'pwned' }, victim)
|
||||
vim.fn.system({ 'tar', '-cPzf', root .. '/abs.tar.gz', victim })
|
||||
|
||||
local result = wait_extract(root .. '/abs.tar.gz', root .. '/dest')
|
||||
assert_true(not result.ok)
|
||||
assert_match('refusing to extract', result.err)
|
||||
assert_match('is absolute or escapes the destination directory', result.err)
|
||||
assert_true(not fsmod.exists(root .. '/dest'))
|
||||
end)
|
||||
|
||||
it('refuses a tarball whose symlink points outside the destination', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/src/plug', 'p')
|
||||
vim.fn.system({ 'ln', '-s', '/etc/passwd', root .. '/src/plug/escape' })
|
||||
vim.fn.system({ 'tar', '-czf', root .. '/link.tar.gz', '-C', root .. '/src', 'plug' })
|
||||
|
||||
local result = wait_extract(root .. '/link.tar.gz', root .. '/dest')
|
||||
assert_true(not result.ok)
|
||||
assert_match('refusing to extract', result.err)
|
||||
assert_match('plug/escape', result.err)
|
||||
assert_true(not fsmod.exists(root .. '/dest/plug'))
|
||||
end)
|
||||
|
||||
it('refuses a tarball whose symlink climbs out with ..', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
|
||||
vim.fn.system({ 'ln', '-s', '../../../outside', root .. '/src/plug/lua/escape' })
|
||||
vim.fn.system({ 'tar', '-czf', root .. '/rel.tar.gz', '-C', root .. '/src', 'plug' })
|
||||
|
||||
local result = wait_extract(root .. '/rel.tar.gz', root .. '/dest')
|
||||
assert_true(not result.ok)
|
||||
assert_match('refusing to extract', result.err)
|
||||
assert_match('plug/lua/escape', result.err)
|
||||
end)
|
||||
|
||||
it('allows a symlink that stays inside the destination', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
|
||||
vim.fn.writefile({ 'return {}' }, root .. '/src/plug/lua/plug.lua')
|
||||
vim.fn.system({ 'ln', '-s', 'lua/plug.lua', root .. '/src/plug/alias' })
|
||||
vim.fn.system({ 'tar', '-czf', root .. '/ok.tar.gz', '-C', root .. '/src', 'plug' })
|
||||
|
||||
local result = wait_extract(root .. '/ok.tar.gz', root .. '/dest')
|
||||
assert_true(result.ok, tostring(result.err))
|
||||
assert_true(fsmod.exists(root .. '/dest/plug/alias'))
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user