Keyboard-driven browser for the small web, with native Gopher and Gemini
fix: escape page data in internal pages, and drop the shell from styles
The bookmarks, history, downloads and log pages built their HTML by
interpolating values straight into a template. Titles were escaped;
uris, tags, dates, domains, ids and log messages were not. All of those
come from the page that was visited, and all four pages export Lua
functions to their own origin, so markup that lands there runs with the
bridge in reach.
The settings page wrote the error text back with innerHTML. It is
textContent now, which cannot carry markup at all.
styles.lua ran its watcher through `bash -c`, interpolating the path
with Lua's %q. That escapes the double quote and the backslash but not
the single quote, and here a real shell was on the other end, so a
stylesheet whose name held one ran the rest of the name as a command.
The watcher now calls inotifywait directly, with no shell, and pauses a
second before retrying when the tool is missing.
The quoting lives in lousy.util.shell_escape next to sql_escape and
lua_escape, since luakit.spawn parses its argument the way a shell would
even when no shell runs. Its test feeds hostile paths through
spawn_sync and checks each one comes back as a single unchanged
argument.
@@ -410,6 +410,22 @@ function _M.sql_escape(s) return "'" .. rstring.gsub(s or "", "'", "''") .. "'"
end
+--- Escape a value for a shell command line.
+--
+-- Wraps the string in single quotes and rewrites any single quote it holds
+-- as <code>'\''</code>, which closes the quoted run, passes one literal
+-- quote, and opens the next run.
+--
+-- `luakit.spawn` splits its argument the way a shell would before handing
+-- the pieces to exec, so even a command that never reaches a shell needs
+-- its arguments quoted.
+--
+-- @tparam string s A string.
+-- @treturn string The quoted string, ready to be concatenated into a command.
+function _M.shell_escape(s)
+ return "'" .. rstring.gsub(tostring(s or ""), "'", "'\\''") .. "'"
+end
+
--- Escape values for lua patterns.
--
-- Escapes the magic characters <code>^$()%.[]*+-?)</code> by prepending a
@@ -369,9 +369,24 @@ -- @tparam table guard a table that controls the watch process. Set `guard[1] -- = nil` to turn off the watch.
-- @tparam string path the path of the watched style.
_M.watch_styles = function (guard, path)
- luakit.spawn(string.format("bash -c 'inotifywait -t 10 %q || sleep 1'", path), function ()
+ local function again ()
_M.detect_files()
if guard[1] then _M.watch_styles(guard, path) end
+ end
+
+ -- This used to run through `bash -c`, interpolating the path with Lua's
+ -- %q, which leaves the single quote alone. A stylesheet whose name held
+ -- one closed the quoting and the rest of the name ran as a command.
+ luakit.spawn("inotifywait -t 10 " .. lousy.util.shell_escape(path), function (_, status)
+ if status == 0 then return again() end
+ -- inotifywait timed out or is not installed: pause before looking
+ -- again, so a missing tool does not spin the loop.
+ local retry = timer{ interval = 1000 }
+ retry:add_signal("timeout", function ()
+ retry:stop()
+ again()
+ end)
+ retry:start()
end)
end
@@ -88,6 +88,30 @@ assert.equal(" %" .. ch .. " ", lousy.util.lua_escape(" " .. ch .. " ")) end
end
+T.test_lousy_util_shell_escape = function ()
+ -- luakit.spawn and luakit.spawn_sync both run the command through
+ -- g_shell_parse_argv, so this checks the real property: whatever goes in
+ -- comes back out as one argument, unchanged and never interpreted.
+ local hostile = {
+ "plain.css",
+ "with space.css",
+ "it's.css",
+ "'; touch /tmp/skull-shell-escape-failed; echo '.css",
+ '"; rm -rf /; echo "',
+ "back\\slash.css",
+ "$HOME/`id`/${PATH}.css",
+ "semi;colon|pipe&.css",
+ }
+
+ for _, path in ipairs(hostile) do
+ local _, out = luakit.spawn_sync("printf %s " .. lousy.util.shell_escape(path))
+ assert.equal(path, out)
+ end
+
+ -- The payload above would have created this file had the quoting failed.
+ assert.is_nil(lfs.attributes("/tmp/skull-shell-escape-failed"))
+end
+
return T
-- vim: et:sw=4:ts=8:sts=4:tw=80