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)
|
||||
|
||||
@@ -126,6 +126,33 @@ describe('install end to end', function()
|
||||
assert_match(missing, results[1].err)
|
||||
end)
|
||||
|
||||
it('installs with the cache on a different filesystem from the pack root', function()
|
||||
local root = tmpdir()
|
||||
local pack_root = vim.fs.joinpath(root, 'pack')
|
||||
local other = otherfs_dir(root)
|
||||
if not other then
|
||||
return
|
||||
end
|
||||
|
||||
local real_cache_dir = fsmod.cache_dir
|
||||
fsmod.cache_dir = function()
|
||||
return vim.fs.joinpath(other, 'vimpack')
|
||||
end
|
||||
|
||||
local archive = make_tarball(root, '1.0.0', 'return 1')
|
||||
local cfg = assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive } },
|
||||
}))
|
||||
local data = manifest.empty()
|
||||
local ok, results = pcall(install, cfg, data, pack_root)
|
||||
fsmod.cache_dir = real_cache_dir
|
||||
fsmod.rm_rf(other)
|
||||
|
||||
assert_true(ok, tostring(results))
|
||||
assert_true(results[1].ok, tostring(results[1].err))
|
||||
assert_eq({ 'return 1' }, vim.fn.readfile(vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')))
|
||||
end)
|
||||
|
||||
it('moves a plugin from start to opt', function()
|
||||
local root = tmpdir()
|
||||
local pack_root = vim.fs.joinpath(root, 'pack')
|
||||
|
||||
@@ -140,4 +140,23 @@ describe('config', function()
|
||||
assert_nil(cfg)
|
||||
assert_match('no registry', err)
|
||||
end)
|
||||
|
||||
it('keeps a well-formed dir_name override', function()
|
||||
local cfg = assert(config.normalize({
|
||||
registry = reg,
|
||||
plugins = { { name = 'oil.nvim', version = '2.11.0', dir_name = 'oil' } },
|
||||
}))
|
||||
assert_eq('oil', cfg.plugins[1].dir_name)
|
||||
end)
|
||||
|
||||
it('rejects a dir_name that is not a single path component', function()
|
||||
for _, bad in ipairs({ '../evil', '/abs', 'a/b', '..', '.', '' }) do
|
||||
local cfg, err = config.normalize({
|
||||
registry = reg,
|
||||
plugins = { { name = 'a', version = '1', dir_name = bad } },
|
||||
})
|
||||
assert_nil(cfg, 'dir_name ' .. vim.inspect(bad) .. ' should be rejected')
|
||||
assert_match('dir_name', err)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -65,6 +65,26 @@ _G.tmpdir = function()
|
||||
return dir
|
||||
end
|
||||
|
||||
-- A writable directory on a filesystem other than `reference`, so the EXDEV
|
||||
-- paths can be exercised for real. nil when the box has no second mount.
|
||||
_G.otherfs_dir = function(reference)
|
||||
local uv = vim.uv or vim.loop
|
||||
local ref = uv.fs_stat(reference)
|
||||
if not ref then
|
||||
return nil
|
||||
end
|
||||
for _, base in ipairs({ '/dev/shm', '/run/user/' .. tostring(uv.getuid and uv.getuid() or 0), '/var/tmp', '/tmp' }) do
|
||||
local stat = uv.fs_stat(base)
|
||||
if stat and stat.dev ~= ref.dev then
|
||||
local dir = vim.fs.joinpath(base, 'vimpack-xdev-' .. tostring(uv.hrtime()))
|
||||
if vim.fn.mkdir(dir, 'p') == 1 then
|
||||
return dir
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local files = {}
|
||||
for i = 1, #_G.arg do
|
||||
files[#files + 1] = _G.arg[i]
|
||||
|
||||
@@ -74,6 +74,28 @@ describe('url escape hatch', function()
|
||||
assert_eq('https://h/x.zip', resolved.url)
|
||||
assert_eq('x.zip', resolved.filename)
|
||||
end)
|
||||
|
||||
it('still applies the registry auth headers', function()
|
||||
vim.env.VIMPACK_TEST_TOKEN = 'sekret'
|
||||
local resolved = assert(
|
||||
registry.resolve(
|
||||
{ name = 'x', version = '1', url = 'https://h/x.zip' },
|
||||
{ auth = { env = 'VIMPACK_TEST_TOKEN' } }
|
||||
)
|
||||
)
|
||||
assert_eq('Bearer sekret', resolved.headers.Authorization)
|
||||
vim.env.VIMPACK_TEST_TOKEN = nil
|
||||
end)
|
||||
|
||||
it('propagates an auth failure instead of downloading unauthenticated', function()
|
||||
vim.env.VIMPACK_TEST_TOKEN = nil
|
||||
local resolved, err = registry.resolve(
|
||||
{ name = 'x', version = '1', url = 'https://h/x.zip' },
|
||||
{ auth = { env = 'VIMPACK_TEST_TOKEN' } }
|
||||
)
|
||||
assert_nil(resolved)
|
||||
assert_match('VIMPACK_TEST_TOKEN', err)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('auth headers', function()
|
||||
|
||||
Reference in New Issue
Block a user