feat(exrc): user must view and explicitly run ":trust" #35069

Problem:
It's relatively easy to mispress key `a` to (a)llow arbitrary execution
of 'exrc' files. #35050

Solution:
- For exrc files (not directories), remove "allow" menu item.
  Require the user to "view" and then explicitly `:trust` the file.
This commit is contained in:
nyngwang
2025-07-29 04:11:58 +08:00
committed by GitHub
parent cf9b36f3d9
commit dc67ba948e
4 changed files with 64 additions and 28 deletions

View File

@@ -63,7 +63,10 @@ DIAGNOSTICS
EDITOR EDITOR
todo |vim.secure.read()| now removes the choice "(a)llow" from the prompt reply for
files unlisted in the user's trust database, and thus requires the user to
choose (v)iew then run `:trust`. Previously the user would be able to press
the single key 'a' to execute the arbitrary execution immediately.
EVENTS EVENTS

View File

@@ -121,18 +121,16 @@ function M.read(path)
return contents return contents
end end
local dir_msg = '' local dir_msg = ' To enable it, choose (v)iew then run `:trust`.'
local choices = '&ignore\n&view\n&deny'
if hash == 'directory' then if hash == 'directory' then
dir_msg = ' DIRECTORY trust is decided only by its name, not its contents.' dir_msg = ' DIRECTORY trust is decided only by its name, not its contents.'
choices = '&ignore\n&view\n&deny\n&allow'
end end
-- File either does not exist in trust database or the hash does not match -- File either does not exist in trust database or the hash does not match
local ok, result = pcall( local ok, result =
vim.fn.confirm, pcall(vim.fn.confirm, string.format('%s is not trusted.%s', fullpath, dir_msg), choices, 1)
string.format('%s is not trusted.%s', fullpath, dir_msg),
'&ignore\n&view\n&deny\n&allow',
1
)
if not ok and result ~= 'Keyboard interrupt' then if not ok and result ~= 'Keyboard interrupt' then
error(result) error(result)
@@ -147,7 +145,7 @@ function M.read(path)
-- Deny -- Deny
trust[fullpath] = '!' trust[fullpath] = '!'
contents = nil contents = nil
elseif result == 4 then elseif hash == 'directory' and result == 4 then
-- Allow -- Allow
trust[fullpath] = hash trust[fullpath] = hash
end end

View File

@@ -1200,9 +1200,11 @@ describe('user config init', function()
VIMRUNTIME = os.getenv('VIMRUNTIME'), VIMRUNTIME = os.getenv('VIMRUNTIME'),
}, },
}) })
screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny, (a)llow:') }) screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny:') })
-- `i` to enter Terminal mode, `a` to allow -- `i` to enter Terminal mode, `v` to view then `:trust`
feed('ia') feed('iv')
feed(':trust<CR>')
feed(':q<CR>')
screen:expect([[ screen:expect([[
^ | ^ |
~ |*4 ~ |*4
@@ -1219,8 +1221,8 @@ describe('user config init', function()
%s%s| %s%s|
-- TERMINAL -- | -- TERMINAL -- |
]], ]],
filename, '---',
string.rep(' ', 50 - #filename) string.rep(' ', 50 - #'---')
)) ))
clear { args_rm = { '-u' }, env = xstateenv } clear { args_rm = { '-u' }, env = xstateenv }
@@ -1239,7 +1241,8 @@ describe('user config init', function()
setup_exrc_file('.nvim.lua') setup_exrc_file('.nvim.lua')
setup_exrc_file('../.exrc') setup_exrc_file('../.exrc')
clear { args_rm = { '-u' }, env = xstateenv } clear { args_rm = { '-u' }, env = xstateenv }
local screen = Screen.new(50, 8) -- use a screen wide width to avoid wrapping the word `.exrc`, `.nvim.lua` below.
local screen = Screen.new(500, 8)
screen._default_attr_ids = nil screen._default_attr_ids = nil
fn.jobstart({ nvim_prog }, { fn.jobstart({ nvim_prog }, {
term = true, term = true,
@@ -1249,13 +1252,36 @@ describe('user config init', function()
}) })
-- current directory exrc is found first -- current directory exrc is found first
screen:expect({ any = '.nvim.lua' }) screen:expect({ any = '.nvim.lua' })
screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny, (a)llow:'), unchanged = true }) screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny:'), unchanged = true })
feed('ia') feed('iv')
-- after that the exrc in the parent directory -- after that the exrc in the parent directory
screen:expect({ any = '.exrc' }) screen:expect({ any = '.exrc', unchanged = true })
screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny, (a)llow:'), unchanged = true }) screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny:'), unchanged = true })
feed('a') feed('v')
-- trust .exrc
feed(':trust<CR>')
screen:expect({ any = 'Allowed ".*' .. pathsep .. '%.exrc" in trust database.' })
feed(':q<CR>')
-- trust .nvim.lua
feed(':trust<CR>')
screen:expect({ any = 'Allowed ".*' .. pathsep .. '%.nvim%.lua" in trust database.' })
feed(':q<CR>')
-- no exrc file is executed
feed(':echo g:exrc_count<CR>')
screen:expect({ any = 'E121: Undefined variable: g:exrc_count' })
-- restart nvim
feed(':restart<CR>')
screen:expect([[
^{MATCH: +}|
~{MATCH: +}|*4
[No Name]{MATCH: +}0,0-1{MATCH: +}All|
{MATCH: +}|
-- TERMINAL --{MATCH: +}|
]])
-- a total of 2 exrc files are executed -- a total of 2 exrc files are executed
feed(':echo g:exrc_count<CR>') feed(':echo g:exrc_count<CR>')
screen:expect({ any = '2' }) screen:expect({ any = '2' })

View File

@@ -55,7 +55,9 @@ describe('vim.secure', function()
}) })
local cwd = fn.getcwd() local cwd = fn.getcwd()
local msg = cwd .. pathsep .. 'Xfile is not trusted.' local msg = cwd
.. pathsep
.. 'Xfile is not trusted. To enable it, choose (v)iew then run `:trust`.'
if #msg >= screen._width then if #msg >= screen._width then
pending('path too long') pending('path too long')
return return
@@ -69,7 +71,7 @@ describe('vim.secure', function()
{2:{MATCH: +}}| {2:{MATCH: +}}|
:lua vim.secure.read('Xfile'){MATCH: +}| :lua vim.secure.read('Xfile'){MATCH: +}|
{3:]] .. msg .. [[}{MATCH: +}| {3:]] .. msg .. [[}{MATCH: +}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}| {3:[i]gnore, (v)iew, (d)eny: }^{MATCH: +}|
]]) ]])
feed('d') feed('d')
screen:expect([[ screen:expect([[
@@ -91,14 +93,21 @@ describe('vim.secure', function()
{2:{MATCH: +}}| {2:{MATCH: +}}|
:lua vim.secure.read('Xfile'){MATCH: +}| :lua vim.secure.read('Xfile'){MATCH: +}|
{3:]] .. msg .. [[}{MATCH: +}| {3:]] .. msg .. [[}{MATCH: +}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}| {3:[i]gnore, (v)iew, (d)eny: }^{MATCH: +}|
]]) ]])
feed('a') feed('v')
feed(':trust<CR>')
screen:expect([[ screen:expect([[
^{MATCH: +}| ^let g:foobar = 42{MATCH: +}|
{1:~{MATCH: +}}|*6 {1:~{MATCH: +}}|*2
{2:]] .. fn.fnamemodify(cwd, ':~') .. pathsep .. [[Xfile [RO]{MATCH: +}}|
{MATCH: +}| {MATCH: +}|
{1:~{MATCH: +}}|
{4:[No Name]{MATCH: +}}|
Allowed "]] .. cwd .. pathsep .. [[Xfile" in trust database.{MATCH: +}|
]]) ]])
-- close the split for the next test below.
feed(':q<CR>')
local hash = fn.sha256(assert(read_file('Xfile'))) local hash = fn.sha256(assert(read_file('Xfile')))
trust = assert(read_file(stdpath('state') .. pathsep .. 'trust')) trust = assert(read_file(stdpath('state') .. pathsep .. 'trust'))
@@ -114,7 +123,7 @@ describe('vim.secure', function()
{2:{MATCH: +}}| {2:{MATCH: +}}|
:lua vim.secure.read('Xfile'){MATCH: +}| :lua vim.secure.read('Xfile'){MATCH: +}|
{3:]] .. msg .. [[}{MATCH: +}| {3:]] .. msg .. [[}{MATCH: +}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}| {3:[i]gnore, (v)iew, (d)eny: }^{MATCH: +}|
]]) ]])
feed('i') feed('i')
screen:expect([[ screen:expect([[
@@ -133,7 +142,7 @@ describe('vim.secure', function()
{2:{MATCH: +}}| {2:{MATCH: +}}|
:lua vim.secure.read('Xfile'){MATCH: +}| :lua vim.secure.read('Xfile'){MATCH: +}|
{3:]] .. msg .. [[}{MATCH: +}| {3:]] .. msg .. [[}{MATCH: +}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}| {3:[i]gnore, (v)iew, (d)eny: }^{MATCH: +}|
]]) ]])
feed('v') feed('v')
screen:expect([[ screen:expect([[