feat(secure): add :trust command and vim.secure.trust() (#21107)

Introduce vim.secure.trust() to programmatically manage the trust
database. Use this function in a new :trust ex command which can
be used as a simple frontend.

Resolves: https://github.com/neovim/neovim/issues/21092
Co-authored-by: Gregory Anders <greg@gpanders.com>
Co-authored-by: ii14 <ii14@users.noreply.github.com>
This commit is contained in:
Jlll1
2022-11-28 20:23:04 +01:00
committed by GitHub
parent 77a0f4a542
commit f004812b33
13 changed files with 541 additions and 29 deletions

View File

@@ -1650,4 +1650,32 @@ There are three different types of searching:
currently work with 'path' items that contain a URL or use the double star
with depth limiter (/usr/**2) or upward search (;) notations.
==============================================================================
11. Trusted Files *trust*
Nvim has the ability to execute arbitrary code through the 'exrc' option. In
order to prevent executing code from untrusted sources, Nvim has the concept of
"trusted files". An untrusted file will not be executed without the user's
consent, and a user can permanently mark a file as trusted or untrusted using
the |:trust| command or the |vim.secure.read()| function.
*:trust* *E5570*
:trust [++deny] [++remove] [{file}]
Manage files in the trust database. Without any options
or arguments, :trust adds the file associated with the
current buffer to the trust database, along with the
SHA256 hash of its contents.
[++deny] marks the file associated with the current
buffer (or {file}, if given) as denied; no prompts will
be displayed to the user and the file will never be
executed.
[++remove] removes the file associated with the current
buffer (or {file}, if given) from the trust database.
Future attempts to read the file in a secure setting
(i.e. with 'exrc' or |vim.secure.read()|) will prompt
the user if the file is trusted.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@@ -1633,6 +1633,7 @@ tag command action ~
|:topleft| :to[pleft] make split window appear at top or far left
|:tprevious| :tp[revious] jump to previous matching tag
|:trewind| :tr[ewind] jump to first matching tag
|:trust| :trust add or remove file from trust database
|:try| :try execute commands, abort on error or exception
|:tselect| :ts[elect] list matching tags and select one
|:tunmap| :tunma[p] like ":unmap" but for |Terminal-mode|

View File

@@ -2371,4 +2371,28 @@ read({path}) *vim.secure.read()*
(string|nil) The contents of the given file if it exists and is
trusted, or nil otherwise.
See also: ~
|:trust|
trust({opts}) *vim.secure.trust()*
Manage the trust database.
The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
Parameters: ~
• {opts} (table)
• action (string): "allow" to add a file to the trust database
and trust it, "deny" to add a file to the trust database and
deny it, "remove" to remove file from the trust database
• path (string|nil): Path to a file to update. Mutually
exclusive with {bufnr}. Cannot be used when {action} is
"allow".
• bufnr (number|nil): Buffer number to update. Mutually
exclusive with {path}.
Return: ~
(boolean, string) success, msg:
• true and full path of target file if operation was successful
• false and error message on failure
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:

View File

@@ -39,6 +39,9 @@ NEW FEATURES *news-features*
The following new APIs or features were added.
• |vim.secure.trust()|, |:trust| allows the user to manage files in trust
database.
• |vim.diagnostic.open_float()| (and therefore |vim.diagnostic.config()|) now
accepts a `suffix` option which, by default, renders LSP error codes.
Similarly, the `virtual_text` configuration in |vim.diagnostic.config()| now

View File

@@ -2275,6 +2275,8 @@ A jump table for the options with a short description can be found at |Q_op|.
file are persisted to a trust database. The user is only prompted
again if the file contents change. See |vim.secure.read()|.
Use |:trust| to manage the trusted file database.
This option cannot be set from a |modeline| or in the |sandbox|, for
security reasons.

View File

@@ -1,9 +1,50 @@
local M = {}
---@private
--- Reads trust database from $XDG_STATE_HOME/nvim/trust.
---
---@return (table) Contents of trust database, if it exists. Empty table otherwise.
local function read_trust()
local trust = {}
local f = io.open(vim.fn.stdpath('state') .. '/trust', 'r')
if f then
local contents = f:read('*a')
if contents then
for line in vim.gsplit(contents, '\n') do
local hash, file = string.match(line, '^(%S+) (.+)$')
if hash and file then
trust[file] = hash
end
end
end
f:close()
end
return trust
end
---@private
--- Writes provided {trust} table to trust database at
--- $XDG_STATE_HOME/nvim/trust.
---
---@param trust (table) Trust table to write
local function write_trust(trust)
vim.validate({ trust = { trust, 't' } })
local f = assert(io.open(vim.fn.stdpath('state') .. '/trust', 'w'))
local t = {}
for p, h in pairs(trust) do
t[#t + 1] = string.format('%s %s\n', h, p)
end
f:write(table.concat(t))
f:close()
end
--- Attempt to read the file at {path} prompting the user if the file should be
--- trusted. The user's choice is persisted in a trust database at
--- $XDG_STATE_HOME/nvim/trust.
---
---@see |:trust|
---
---@param path (string) Path to a file to read.
---
---@return (string|nil) The contents of the given file if it exists and is
@@ -15,22 +56,7 @@ function M.read(path)
return nil
end
local trust = {}
do
local f = io.open(vim.fn.stdpath('state') .. '/trust', 'r')
if f then
local contents = f:read('*a')
if contents then
for line in vim.gsplit(contents, '\n') do
local hash, file = string.match(line, '^(%S+) (.+)$')
if hash and file then
trust[file] = hash
end
end
end
f:close()
end
end
local trust = read_trust()
if trust[fullpath] == '!' then
-- File is denied
@@ -86,21 +112,82 @@ function M.read(path)
trust[fullpath] = hash
end
do
local f, err = io.open(vim.fn.stdpath('state') .. '/trust', 'w')
if not f then
error(err)
end
local t = {}
for p, h in pairs(trust) do
t[#t + 1] = string.format('%s %s\n', h, p)
end
f:write(table.concat(t))
f:close()
end
write_trust(trust)
return contents
end
--- Manage the trust database.
---
--- The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
---
---@param opts (table):
--- - action (string): "allow" to add a file to the trust database and trust it,
--- "deny" to add a file to the trust database and deny it,
--- "remove" to remove file from the trust database
--- - path (string|nil): Path to a file to update. Mutually exclusive with {bufnr}.
--- Cannot be used when {action} is "allow".
--- - bufnr (number|nil): Buffer number to update. Mutually exclusive with {path}.
---@return (boolean, string) success, msg:
--- - true and full path of target file if operation was successful
--- - false and error message on failure
function M.trust(opts)
vim.validate({
path = { opts.path, 's', true },
bufnr = { opts.bufnr, 'n', true },
action = {
opts.action,
function(m)
return m == 'allow' or m == 'deny' or m == 'remove'
end,
[["allow" or "deny" or "remove"]],
},
})
local path = opts.path
local bufnr = opts.bufnr
local action = opts.action
if path and bufnr then
error('path and bufnr are mutually exclusive', 2)
end
local fullpath
if path then
fullpath = vim.loop.fs_realpath(vim.fs.normalize(path))
else
local bufname = vim.api.nvim_buf_get_name(bufnr)
if bufname == '' then
return false, 'buffer is not associated with a file'
end
fullpath = vim.loop.fs_realpath(vim.fs.normalize(bufname))
end
if not fullpath then
return false, string.format('invalid path: %s', path)
end
local trust = read_trust()
if action == 'allow' then
assert(bufnr, 'bufnr is required when action is "allow"')
local newline = vim.bo[bufnr].fileformat == 'unix' and '\n' or '\r\n'
local contents = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), newline)
if vim.bo[bufnr].endofline then
contents = contents .. newline
end
local hash = vim.fn.sha256(contents)
trust[fullpath] = hash
elseif action == 'deny' then
trust[fullpath] = '!'
elseif action == 'remove' then
trust[fullpath] = nil
end
write_trust(trust)
return true, fullpath
end
return M