From: mdevolde Date: Mon, 24 Nov 2025 15:12:23 +0000 (+0100) Subject: luci-app-wol: replace fs.stat/exec with safe RPC backend, fix ACLs X-Git-Url: http://git.openwrt.org/?a=commitdiff_plain;h=bf7d15af13a9610512e585b45fe3c5bd1aebf285;p=project%2Fluci.git luci-app-wol: replace fs.stat/exec with safe RPC backend, fix ACLs fs.stat() and fs.exec() require broad rpcd permissions (ubus file.*) which prevent luci-app-wol from working for restricted users and expose more access than necessary. This introduces a dedicated RPC backend (luci.wol) providing safe stat/exec wrappers for etherwake and wakeonlan, and simplifies ACLs to only allow these two RPC calls. Also adds the missing 'getNetworkDevices' ACL required by DeviceSelect. Signed-off-by: Martin Devolder --- diff --git a/applications/luci-app-wol/htdocs/luci-static/resources/view/wol.js b/applications/luci-app-wol/htdocs/luci-static/resources/view/wol.js index 29b0d305aa..f67e9d1ccf 100644 --- a/applications/luci-app-wol/htdocs/luci-static/resources/view/wol.js +++ b/applications/luci-app-wol/htdocs/luci-static/resources/view/wol.js @@ -8,9 +8,26 @@ 'require form'; 'require tools.widgets as widgets'; +const ETHERWAKE_BIN = '/usr/bin/etherwake'; +const WAKEONLAN_BIN = '/usr/bin/wakeonlan'; + return view.extend({ formdata: { wol: {} }, + callStat: rpc.declare({ + object: 'luci.wol', + method: 'stat', + params: [ ], + expect: { } + }), + + callExec: rpc.declare({ + object: 'luci.wol', + method: 'exec', + params: [ 'name', 'args' ], + expect: { } + }), + callHostHints: rpc.declare({ object: 'luci-rpc', method: 'getHostHints', @@ -19,17 +36,15 @@ return view.extend({ load: function() { return Promise.all([ - L.resolveDefault(fs.stat('/usr/bin/etherwake')), - L.resolveDefault(fs.stat('/usr/bin/wol')), + L.resolveDefault(this.callStat()), this.callHostHints(), uci.load('etherwake') ]); }, - render: function(data) { - var has_ewk = data[0], - has_wol = data[1], - hosts = data[2], + render([stat, hosts]) { + var has_ewk = stat && stat.etherwake, + has_wol = stat && stat.wakeonlan, m, s, o; this.formdata.has_ewk = has_ewk; @@ -44,8 +59,8 @@ return view.extend({ o = s.option(form.ListValue, 'executable', _('WoL program'), _('Sometimes only one of the two tools works. If one fails, try the other one')); - o.value('/usr/bin/etherwake', 'Etherwake'); - o.value('/usr/bin/wol', 'WoL'); + o.value(ETHERWAKE_BIN, 'Etherwake'); + o.value(WAKEONLAN_BIN, 'Wakeonlan'); } if (has_ewk) { @@ -67,7 +82,7 @@ return view.extend({ }); if (has_wol) - o.depends('executable', '/usr/bin/etherwake'); + o.depends('executable', ETHERWAKE_BIN); } o = s.option(form.Value, 'mac', _('Host to wake up'), @@ -88,7 +103,7 @@ return view.extend({ o = s.option(form.Flag, 'broadcast', _('Send to broadcast address')); if (has_wol) - o.depends('executable', '/usr/bin/etherwake'); + o.depends('executable', ETHERWAKE_BIN); } return m.render(); @@ -96,16 +111,17 @@ return view.extend({ handleWakeup: function(ev) { var map = document.querySelector('#maincontent .cbi-map'), - data = this.formdata; + data = this.formdata, + self = this; return dom.callClassMethod(map, 'save').then(function() { if (!data.wol.mac) return alert(_('No target host specified!')); - var bin = data.executable || (data.has_ewk ? '/usr/bin/etherwake' : '/usr/bin/wol'), + var bin = data.wol.executable || (data.has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN), args = []; - if (bin == '/usr/bin/etherwake') { + if (bin == ETHERWAKE_BIN) { args.push('-D', '-i', data.wol.iface); if (data.wol.broadcast == '1') @@ -114,16 +130,16 @@ return view.extend({ args.push(data.wol.mac); } else { - args.push('-v', data.wol.mac); + args.push(data.wol.mac); } ui.showModal(_('Waking host'), [ E('p', { 'class': 'spinning' }, [ _('Starting WoL utility…') ]) ]); - - return fs.exec(bin, args).then(function(res) { + + return self.callExec(bin, args).then(function(res) { ui.showModal(_('Waking host'), [ - res.stderr ? E('p', [ res.stdout ]) : '', + res.stdout ? E('p', [ res.stdout ]) : '', res.stderr ? E('pre', [ res.stderr ]) : '', E('div', { 'class': 'right' }, [ E('button', { diff --git a/applications/luci-app-wol/root/usr/share/rpcd/acl.d/luci-app-wol.json b/applications/luci-app-wol/root/usr/share/rpcd/acl.d/luci-app-wol.json index c679fe3213..b2bece606c 100644 --- a/applications/luci-app-wol/root/usr/share/rpcd/acl.d/luci-app-wol.json +++ b/applications/luci-app-wol/root/usr/share/rpcd/acl.d/luci-app-wol.json @@ -3,14 +3,14 @@ "description": "Grant access to wake-on-lan executables", "read": { "ubus": { - "luci-rpc": [ "getHostHints" ] + "luci.wol": [ "stat" ], + "luci-rpc": [ "getHostHints", "getNetworkDevices" ] }, "uci": [ "etherwake" ] }, "write": { - "file": { - "/usr/bin/etherwake": [ "exec" ], - "/usr/bin/wol": [ "exec" ] + "ubus": { + "luci.wol": [ "exec" ] } } } diff --git a/applications/luci-app-wol/root/usr/share/rpcd/ucode/luci.wol b/applications/luci-app-wol/root/usr/share/rpcd/ucode/luci.wol new file mode 100644 index 0000000000..9d1e26f1c2 --- /dev/null +++ b/applications/luci-app-wol/root/usr/share/rpcd/ucode/luci.wol @@ -0,0 +1,57 @@ +#!/usr/bin/ucode + +'use strict'; + +import { access, stat, popen } from 'fs'; + +const etherwake = '/usr/bin/etherwake'; +const wakeonlan = '/usr/bin/wakeonlan'; + + +function shellquote(s) { + return "'" + replace(s, "'", "'\\''") + "'"; +} + + +const methods = { + stat: { + call: function(request) { + const result = {}; + + result.etherwake = false; + result.wakeonlan = false; + + if (access(etherwake, "x")) { + result.etherwake = true; + } + + if (access(wakeonlan, "x")) { + result.wakeonlan = true; + } + + return result; + } + }, + + exec: { + args: { name: 'string', args: [] }, + call: function(request) { + const result = {}; + if (request.args.name == etherwake || request.args.name == wakeonlan) { + parts = map(request.args.args, shellquote); + const fd = popen(request.args.name + ' ' + join(' ', parts)); + + result.stdout = fd.read('all'); + result.stderr = ''; + result.code = 0; + } else { + result.stdout = ''; + result.stderr = 'disallowed'; + result.code = 1; + } + return result; + } + } +}; + +return { "luci.wol": methods };