P1-1 Wi-Fi SSID/PSK 注入(netmgrd + launcherd 回退两处同步):
- 不再拼 `set_network %d ssid '"%s"'` 交给 sh -c;新增 wpa_exec()/nm_direct_wpa_exec()
以 fork+execv 传 argv(含 resolve_bin 解析绝对路径,替代 `PATH=...` 前缀)。
- 共用 nm_wifi_args.h:nm_wifi_ssid_ok/n_wifi_psk_ok/nm_wpa_quote/nm_wpa_set_argv,
两份实现同一套规则(只修一处等于没修)。
- 约束层:SSID 1..32B、禁控制字符;PSK ≤63 或 64 hex、禁控制字符;`"`/`\` 按
wpa_supplicant 语法转义。shell 元字符不再需要拒绝——execv 下没有 shell 解析。
P1-2 MeowPanel App 安装/导出/卸载注入:
- install/export/remove 改 run_argv()(fork+execv,PATH 只在子进程 setenv)。
- 包名统一 mp_pkg_ok()(仅 [a-z0-9._-],拒 `..`,与 nyai.c valid_pkg 同规则)→ 修 P2-7。
- `{path}` 追加 mp_path_unsafe()(与 mp_fonts/nyai 同一拒绝集)作第二道。
P1-3 /api/key/* 电源键:
- keycode_to_device 删除 KEY_POWER;116 进红线(403),前端遥控板电源/长按电源按钮移除。
- press_ms 全局 clamp 0..1000(单键与批量),`long` 缺省取上限。
- 按下→等待→抬起改独立线程(inject_key_async),不再阻塞单线程 8080 服务。
测试(宿主,反向控制):test_nm_wifi_args.c / test_mp_argsafe.c —— 同一套审计恶意串
(`a' ; id ; '`、`x';id;'`、`/tmp/a';id>/tmp/pwn;'.mpk`、`MeowAPPs/..`)在修复前实现下
注入成功(stdout 含 uid=)/放行,在修复后实现下不执行/拒绝。另含「两处实现同步」检查。
未刷机、未连设备、未发 /api/key 请求;只改源码,未提交二进制。
251 lines
10 KiB
C
251 lines
10 KiB
C
/* mp_apps.c — 应用域:MeowAPPs 扫描 + nyai 安装/导出/移除。 */
|
||
#include "mp.h"
|
||
#include "mp_argsafe.h" /* M64 P1-2:包名/路径判定(与 nyai.c:125/137 同规则) */
|
||
|
||
|
||
int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail)
|
||
{
|
||
char pj[1024];
|
||
snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name);
|
||
char buf[8192];
|
||
if (read_small_file(pj, buf, sizeof buf) <= 0) {
|
||
/* 解析失败项:invalid */
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, name);
|
||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||
}
|
||
return -1;
|
||
}
|
||
jv_t *root = jv_parse(buf, (int)strlen(buf));
|
||
if (!root) {
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, name);
|
||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||
}
|
||
return -1;
|
||
}
|
||
jv_t *jpkg = jv_obj_get(root, "package");
|
||
jv_t *jname = jv_obj_get(root, "name");
|
||
jv_t *jver = jv_obj_get(root, "version");
|
||
jv_t *jexec = jv_obj_get(root, "exec");
|
||
jv_t *jfw = jv_obj_get(root, "framework");
|
||
jv_t *jim = jv_obj_get(root, "immersive");
|
||
jv_t *jfull = jv_obj_get(root, "fullscreen");
|
||
const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name;
|
||
const char *nm = (jname && jname->type == J_STR) ? jname->str : "";
|
||
const char *ver = (jver && jver->type == J_STR) ? jver->str : "";
|
||
const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : "";
|
||
/* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */
|
||
long long dsize = 0;
|
||
snprintf(pj, sizeof pj, "%s/%s", appdir, name);
|
||
DIR *d = opendir(pj);
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) == 0) dsize += (long long)st.st_size;
|
||
}
|
||
closedir(d);
|
||
}
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, pkg);
|
||
sA(out, ",\"name\":");
|
||
sA_str(out, nm[0] ? nm : NULL);
|
||
sA(out, ",\"version\":");
|
||
sA_str(out, ver[0] ? ver : NULL);
|
||
sA(out, ",\"exec\":");
|
||
sA_str(out, exec);
|
||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}",
|
||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL));
|
||
} else {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, pkg);
|
||
sA(out, ",\"name\":");
|
||
sA_str(out, nm[0] ? nm : NULL);
|
||
sA(out, ",\"version\":");
|
||
sA_str(out, ver[0] ? ver : NULL);
|
||
sA(out, ",\"exec\":");
|
||
sA_str(out, exec);
|
||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":",
|
||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize);
|
||
sA_str(out, pj);
|
||
sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}");
|
||
}
|
||
jv_free(root);
|
||
return 0;
|
||
}
|
||
|
||
int api_apps_list(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
DIR *d = opendir(MEOWAPPS_DIR);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"count\":0,\"apps\":[");
|
||
int first = 1, count = 0;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue;
|
||
if (strstr(e->d_name, ".bak")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0);
|
||
count++;
|
||
}
|
||
closedir(d);
|
||
sA(&s, "]}");
|
||
/* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */
|
||
/* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
(void)count;
|
||
return rc;
|
||
}
|
||
|
||
int api_apps_info(int sock, const char *pkg)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
/* M64 P1-2 / P2-7:旧判定只拒 `/` 与 `:`,`..` 与 `x';id;'` 都能过。 */
|
||
if (!mp_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char appdir[1024];
|
||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg);
|
||
struct stat st;
|
||
if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装");
|
||
sbuf_t s = {0};
|
||
if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0)
|
||
return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
const char *nyai_bin(void)
|
||
{
|
||
/* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */
|
||
if (!access(NYAI_PATH, X_OK)) return NYAI_PATH;
|
||
return NYAI_PATH_CARD;
|
||
}
|
||
|
||
int api_apps_install(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (access(nyai_bin(), X_OK) < 0) return send_err(sock, 500, "INTERNAL", "nyai 不可用(固件与卡上均无)");
|
||
char mpkpath[1024];
|
||
int ispath = 0;
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) {
|
||
/* JSON {path} */
|
||
if (r->body && r->body[0]) {
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
if (pj && pj->type == J_STR) {
|
||
snprintf(mpkpath, sizeof mpkpath, "%s", pj->str);
|
||
ispath = 1;
|
||
}
|
||
jv_free(root);
|
||
}
|
||
if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file");
|
||
} else {
|
||
/* 保存上传 file 到临时文件 */
|
||
snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR);
|
||
mkdir(mpkpath, 0777);
|
||
char tmp[1024];
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR);
|
||
if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败");
|
||
snprintf(mpkpath, sizeof mpkpath, "%s", tmp);
|
||
}
|
||
if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在");
|
||
/* M64 P1-2:`{path}` 是外部可控串(旧实现直接塞进 popen 的 shell 行)。execv 传入后
|
||
* 元字符已无语义;这里仍按 nyai/mp_fonts 同一拒绝集挡住,防调用被改回 run_cmd。 */
|
||
if (mp_path_unsafe(mpkpath)) return send_err(sock, 400, "BAD_REQUEST", "路径含非法字符");
|
||
char out[4096];
|
||
const char *argv[] = { nyai_bin(), "install", mpkpath, NULL };
|
||
int code = run_argv(out, sizeof out, argv);
|
||
if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败");
|
||
/* 包名 = 文件名茎;版本从 app.json 读 */
|
||
char stem[256];
|
||
const char *base = strrchr(mpkpath, '/');
|
||
base = base ? base + 1 : mpkpath;
|
||
snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base);
|
||
char *dot = strrchr(stem, '.');
|
||
if (dot) *dot = 0;
|
||
char appdir[1024], pj[1100];
|
||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem);
|
||
jv_t *approot = NULL;
|
||
char version[16] = "";
|
||
int upgraded = 0;
|
||
snprintf(pj, sizeof pj, "%s/app.json", appdir);
|
||
char jbuf[8192];
|
||
if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0)
|
||
approot = jv_parse(jbuf, (int)strlen(jbuf));
|
||
if (approot) {
|
||
jv_t *v = jv_obj_get(approot, "version");
|
||
if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str);
|
||
jv_free(approot);
|
||
}
|
||
if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1;
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"package\":");
|
||
sA_str(&s, stem);
|
||
sA(&s, ",\"version\":");
|
||
sA_str(&s, version[0] ? version : NULL);
|
||
sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
int api_apps_export(int sock, const char *pkg)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (!mp_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char tmp[1100];
|
||
char card_tmp[1200];
|
||
snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR);
|
||
mkdir(card_tmp, 0777);
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
|
||
unlink(tmp);
|
||
char out[1024];
|
||
const char *argv[] = { nyai_bin(), "export", pkg, tmp, NULL };
|
||
int code = run_argv(out, sizeof out, argv);
|
||
if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败");
|
||
char dn[256];
|
||
snprintf(dn, sizeof dn, "%s.mpk", pkg);
|
||
return send_file(sock, 200, "application/octet-stream", tmp, dn);
|
||
}
|
||
|
||
int api_apps_remove(int sock, const char *pkg, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (!mp_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char purgebuf[8] = "0";
|
||
if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf);
|
||
int use_purge = atoi(purgebuf);
|
||
char out[1024];
|
||
const char *argv[6];
|
||
int n = 0;
|
||
argv[n++] = nyai_bin();
|
||
argv[n++] = "remove";
|
||
argv[n++] = pkg;
|
||
if (use_purge) argv[n++] = "--purge";
|
||
argv[n] = NULL;
|
||
int code = run_argv(out, sizeof out, argv);
|
||
if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|