mirror of
https://github.com/openwrt/luci.git
synced 2026-05-31 02:21:50 +08:00
luci-app-lxc: fix authenticated path traversal and ACL bypass (host root)
* ucode fixes:
- tighten `is_valid_lxc_name` regex to `^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$`
- apply the validator in `lxc_configuration_get` and `lxc_configuration_set` before any filesystem access
- reject the `'lxc error: …'` sentinel string returned by `lxc_get_config_path()` on failure,
rather than concatenating it into a path.
- shellquote `LXC_URL` in `lxc_get_downloadable` and `lxc_create`
* ACL fix: add `depends.acl = ["luci-app-lxc"]` to each of the five backend entries,
so the routes share the same authorization gate as the view
Signed-off-by: Dirk Brenken <dev@brenken.org>
This commit is contained in:
committed by
Paul Donald
parent
63ed3e6388
commit
fdc8eb01d0
@@ -11,7 +11,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"admin/services/lxccm/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
@@ -20,66 +19,103 @@
|
||||
"path": "lxc/overview"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-lxc" ]
|
||||
"acl": [
|
||||
"luci-app-lxc"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"admin/services/lxc/lxc_create/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.lxc",
|
||||
"function": "lxc_create"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-lxc"
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
|
||||
"admin/services/lxc/lxc_action/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.lxc",
|
||||
"function": "lxc_action"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-lxc"
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
|
||||
"admin/services/lxc/lxc_get_downloadable/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.lxc",
|
||||
"function": "lxc_get_downloadable"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-lxc"
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
|
||||
"admin/services/lxc/lxc_configuration_get/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.lxc",
|
||||
"function": "lxc_configuration_get"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-lxc"
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
|
||||
"admin/services/lxc/lxc_configuration_set/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.lxc",
|
||||
"function": "lxc_configuration_set"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-lxc"
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ function shellquote(value) {
|
||||
}
|
||||
|
||||
function is_valid_lxc_name(value) {
|
||||
return type(value) == 'string' && match(value, /^[A-Za-z0-9._-]{1,64}$/) != null;
|
||||
// LXC container names: start with alphanumeric or underscore, followed by
|
||||
// alphanumeric, underscore, dash. No periods, no slashes, no leading dash
|
||||
return type(value) == 'string' && match(value, /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/) != null;
|
||||
}
|
||||
|
||||
function is_valid_lxc_template(value) {
|
||||
@@ -49,7 +51,7 @@ const LXCController = {
|
||||
lxc_get_downloadable: function() {
|
||||
let target = this.lxc_get_arch_target(LXC_URL);
|
||||
let templates = [];
|
||||
let content = fs.popen(`sh /usr/share/lxc/templates/lxc-download --list --server ${LXC_URL} 2>/dev/null`, 'r').read('all');
|
||||
let content = fs.popen(`sh /usr/share/lxc/templates/lxc-download --list --server ${shellquote(LXC_URL)} 2>/dev/null`, 'r').read('all');
|
||||
content = split(content, '\n');
|
||||
for (let line in content) {
|
||||
let arr = match(line, /^(\S+)\s+(\S+)\s+(\S+)\s+default\s+(\S+)\s*$/);
|
||||
@@ -80,7 +82,7 @@ const LXCController = {
|
||||
let arr = match(lxc_template, /^(.+):(.+)$/);
|
||||
let lxc_dist = arr[1], lxc_release = arr[2];
|
||||
|
||||
system(`/usr/bin/lxc-create --quiet --name ${shellquote(lxc_name)} --bdev best --template download -- --dist ${shellquote(lxc_dist)} --release ${shellquote(lxc_release)} --arch ${this.lxc_get_arch_target(LXC_URL)} --server ${LXC_URL}`);
|
||||
system(`/usr/bin/lxc-create --quiet --name ${shellquote(lxc_name)} --bdev best --template download -- --dist ${shellquote(lxc_dist)} --release ${shellquote(lxc_release)} --arch ${this.lxc_get_arch_target(LXC_URL)} --server ${shellquote(LXC_URL)}`);
|
||||
|
||||
while (fs.access(path + lxc_name + '/partial')) {
|
||||
sleep(1000);
|
||||
@@ -123,23 +125,51 @@ const LXCController = {
|
||||
},
|
||||
|
||||
lxc_configuration_get: function(lxc_name) {
|
||||
let content = fs.readfile(this.lxc_get_config_path() + lxc_name + '/config');
|
||||
|
||||
http.prepare_content('text/plain');
|
||||
|
||||
// Re-validate before fs.readfile as lxc_name,
|
||||
// escapes the lxcpath base and reaches arbitrary files
|
||||
if (!is_valid_lxc_name(lxc_name)) {
|
||||
http.status(400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
// lxc_get_config_path() returns an 'lxc error: …' string on failure
|
||||
// rather than null. Refuse to proceed instead of concatenating that
|
||||
// into a real filesystem path
|
||||
let base = this.lxc_get_config_path();
|
||||
if (!base || index(base, 'lxc error') == 0) {
|
||||
http.status(500, base);
|
||||
return;
|
||||
}
|
||||
let content = fs.readfile(base + lxc_name + '/config');
|
||||
http.write(content);
|
||||
},
|
||||
|
||||
lxc_configuration_set: function(lxc_name) {
|
||||
http.prepare_content('text/plain');
|
||||
|
||||
// Re-validate before fs.writefile as lxc_name,
|
||||
// escapes the lxcpath
|
||||
if (!is_valid_lxc_name(lxc_name)) {
|
||||
http.status(400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
// lxc_get_config_path() returns an 'lxc error: …' string on failure
|
||||
// rather than null. Refuse to proceed instead of concatenating that
|
||||
// into a real filesystem path
|
||||
let base = this.lxc_get_config_path();
|
||||
if (!base || index(base, 'lxc error') == 0) {
|
||||
http.status(500, base);
|
||||
return;
|
||||
}
|
||||
let lxc_configuration = http.formvalue('lxc_conf');
|
||||
lxc_configuration = http.urldecode(lxc_configuration, true);
|
||||
if (!lxc_configuration) {
|
||||
return 'lxc error: config formvalue is empty';
|
||||
}
|
||||
|
||||
fs.writefile(this.lxc_get_config_path() + lxc_name + '/config', lxc_configuration);
|
||||
|
||||
fs.writefile(base + lxc_name + '/config', lxc_configuration);
|
||||
http.write('0');
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user