mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 07:00:26 +02:00
Medium hardening: JSON escapes, policy whitelist, scan lock, EVP check
json_get_str now decodes the common JSON escapes (\" \\ \/ \n \r \t \b \f) and collapses \uXXXX to '?'. Previously \" terminated the value early and \\ was copied literal, so a body containing escapes turned into garbage at the install backend. default_policy is constrained to "allow" or "deny" before being stored, so a malformed POST can't write an arbitrary string into config.json and round-trip it back out of /api/config as broken JSON. /api/manifest/<tid>, /api/pkgverify/<tid>, /api/pkgmeta/<tid> now run path_segment_safe(tid) explicitly. The lookup gate they relied on (get_title_action_info) is defense-by-coincidence — a future refactor that populates g_titles via another path would lose the check. patchdl_scan and patchdl_scan_debug_json perform a process-wide vnode swap that is only safe single-threaded. patchdl_scan_lock() is now called once right after MHD_start_daemon; subsequent calls return -1 / NULL instead of racing the worker threads. EVP_DigestFinal_ex return code is now checked. A failed final left dig uninitialized; hex_encode would have produced empty hex and a silent -2 with no diagnostic. patchdl_sha256_fd_region switches its inner sprintf to snprintf — same effect, no -Wformat-security warning.
This commit is contained in:
1 parent
fcb0a5c3b0
commit
73c75ddd83
4 files changed
+74
-6
No files matched your search
+9
-4
@@ -488,8 +488,11 @@ http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out,
|
||||
unsigned char dig[EVP_MAX_MD_SIZE];
|
||||
unsigned int dlen = 0;
|
||||
char hex[2 * EVP_MAX_MD_SIZE + 1];
|
||||
EVP_DigestFinal_ex(sink.md, dig, &dlen);
|
||||
int ok = EVP_DigestFinal_ex(sink.md, dig, &dlen);
|
||||
EVP_MD_CTX_free(sink.md);
|
||||
/* Fail-closed on a digest API failure — otherwise hex would be empty
|
||||
and we'd silently report -2 with no diagnostic. */
|
||||
if (ok != 1 || dlen == 0) return -2;
|
||||
hex_encode(dig, dlen, hex, sizeof(hex));
|
||||
if (strcasecmp(hex, expected_sha256_hex) != 0)
|
||||
return -2; /* integrity mismatch */
|
||||
@@ -893,8 +896,9 @@ patchdl_http_download_piece(const char *url, int fd,
|
||||
unsigned char dig[EVP_MAX_MD_SIZE];
|
||||
unsigned int dl = 0;
|
||||
char hex[2 * EVP_MAX_MD_SIZE + 1];
|
||||
EVP_DigestFinal_ex(sink.md, dig, &dl);
|
||||
int ok = EVP_DigestFinal_ex(sink.md, dig, &dl);
|
||||
EVP_MD_CTX_free(sink.md);
|
||||
if (ok != 1 || dl == 0) return -2;
|
||||
hex_encode(dig, dl, hex, sizeof(hex));
|
||||
if (strcasecmp(hex, expected_sha256_or_null) != 0)
|
||||
return -2; /* integrity mismatch */
|
||||
@@ -929,8 +933,9 @@ patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex
|
||||
{
|
||||
unsigned char dig[EVP_MAX_MD_SIZE];
|
||||
unsigned int dl = 0, i;
|
||||
EVP_DigestFinal_ex(md, dig, &dl);
|
||||
for (i = 0; i < dl; i++) sprintf(out_hex + 2 * i, "%02x", dig[i]);
|
||||
int ok = EVP_DigestFinal_ex(md, dig, &dl);
|
||||
if (ok != 1 || dl == 0) { free(buf); EVP_MD_CTX_free(md); return -1; }
|
||||
for (i = 0; i < dl; i++) snprintf(out_hex + 2 * i, 3, "%02x", dig[i]);
|
||||
out_hex[2 * dl] = '\0';
|
||||
}
|
||||
free(buf);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <dirent.h>
|
||||
#include <limits.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -389,6 +390,15 @@ merge_appdb(patchdl_title_t *arr, size_t cnt) {
|
||||
patchdl_appdb_free(info);
|
||||
}
|
||||
|
||||
/* See patchdl_scan_lock — both vnode-swap entry points return -1 / NULL once
|
||||
this is set so a late rescan call can't race the running MHD threads. */
|
||||
static _Atomic int g_scan_locked = 0;
|
||||
|
||||
void
|
||||
patchdl_scan_lock(void) {
|
||||
atomic_store(&g_scan_locked, 1);
|
||||
}
|
||||
|
||||
int
|
||||
patchdl_scan(patchdl_title_t **titles_out, size_t *count_out) {
|
||||
patchdl_title_t *arr;
|
||||
@@ -400,6 +410,8 @@ patchdl_scan(patchdl_title_t **titles_out, size_t *count_out) {
|
||||
struct statfs *mounts = NULL;
|
||||
int nmounts;
|
||||
|
||||
if (atomic_load(&g_scan_locked)) return -1;
|
||||
|
||||
arr = calloc(MAX_TITLES, sizeof(*arr));
|
||||
if (!arr) return -1;
|
||||
|
||||
@@ -467,6 +479,8 @@ patchdl_scan_debug_json(void) {
|
||||
char tmp[2048];
|
||||
pid_t pid = getpid();
|
||||
intptr_t saved_root = 0, root_vnode;
|
||||
|
||||
if (atomic_load(&g_scan_locked)) return NULL;
|
||||
int using_vswap = 0;
|
||||
struct statfs *mounts = NULL;
|
||||
int nmounts;
|
||||
|
||||
@@ -40,3 +40,9 @@ const char *patchdl_source_str(patchdl_source_t src);
|
||||
/* Diagnostic: malloc'd JSON dump of the mount table + scan-base directory
|
||||
listings. Caller frees. */
|
||||
char *patchdl_scan_debug_json(void);
|
||||
|
||||
/* Mark scan/debug as no longer safe to call (must be set after MHD worker
|
||||
threads come up — patchdl_scan performs a process-wide vnode swap that
|
||||
would race any concurrent thread). After this is set, both entry points
|
||||
return immediately. Call once during startup, after MHD_start_daemon. */
|
||||
void patchdl_scan_lock(void);
|
||||
+45
-2
@@ -162,7 +162,10 @@ json_get_int(const char *s, const char *key, int dflt) {
|
||||
return (int)strtol(p, NULL, 10);
|
||||
}
|
||||
|
||||
/* Find `"key":"value"` and copy value into out. */
|
||||
/* Find `"key":"value"` and copy value into out. Decodes the common JSON
|
||||
string escapes (\" \\ \/ \n \r \t \b \f). \uXXXX is collapsed to '?' —
|
||||
we don't accept multi-byte content in any field here. An unknown escape
|
||||
keeps the payload byte. */
|
||||
static void
|
||||
json_get_str(const char *s, const char *key, char *out, size_t sz) {
|
||||
out[0] = '\0';
|
||||
@@ -177,7 +180,31 @@ json_get_str(const char *s, const char *key, char *out, size_t sz) {
|
||||
if (*p != '"') return;
|
||||
p++;
|
||||
size_t i = 0;
|
||||
while (*p && *p != '"' && i + 1 < sz) out[i++] = *p++;
|
||||
while (*p && *p != '"' && i + 1 < sz) {
|
||||
if (*p == '\\' && p[1]) {
|
||||
p++;
|
||||
switch (*p) {
|
||||
case '"': out[i++] = '"'; break;
|
||||
case '\\': out[i++] = '\\'; break;
|
||||
case '/': out[i++] = '/'; break;
|
||||
case 'n': out[i++] = '\n'; break;
|
||||
case 'r': out[i++] = '\r'; break;
|
||||
case 't': out[i++] = '\t'; break;
|
||||
case 'b': out[i++] = '\b'; break;
|
||||
case 'f': out[i++] = '\f'; break;
|
||||
case 'u':
|
||||
/* \uXXXX: not needed for any field we accept; drop to '?' so
|
||||
neither the surrogate pair nor the hex digits leak. */
|
||||
if (p[1] && p[2] && p[3] && p[4]) p += 4;
|
||||
out[i++] = '?';
|
||||
break;
|
||||
default: out[i++] = *p; break;
|
||||
}
|
||||
p++;
|
||||
} else {
|
||||
out[i++] = *p++;
|
||||
}
|
||||
}
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
@@ -1820,6 +1847,11 @@ handle_config_post(struct MHD_Connection *conn, const char *body) {
|
||||
int mc;
|
||||
|
||||
json_get_str(body, "default_policy", pol, sizeof(pol));
|
||||
/* Only the two real values are accepted; anything else is silently
|
||||
dropped so a malformed POST can't corrupt config.json or the JSON
|
||||
we later round-trip out of /api/config. */
|
||||
if (pol[0] && strcmp(pol, "allow") != 0 && strcmp(pol, "deny") != 0)
|
||||
pol[0] = '\0';
|
||||
|
||||
pthread_mutex_lock(&g_mutex);
|
||||
if (pol[0]) {
|
||||
@@ -2037,6 +2069,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
patchdl_manifest_t mf;
|
||||
jbuf_t j = {0};
|
||||
|
||||
if (!path_segment_safe(tid))
|
||||
return queue_text(conn, MHD_HTTP_FORBIDDEN, "forbidden");
|
||||
if (!get_title_action_info(tid, &src, purl, sizeof purl,
|
||||
durl_, sizeof durl_,
|
||||
pti, sizeof pti,
|
||||
@@ -2074,6 +2108,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
patchdl_manifest_t mf;
|
||||
jbuf_t j = {0};
|
||||
|
||||
if (!path_segment_safe(tid))
|
||||
return queue_text(conn, MHD_HTTP_FORBIDDEN, "forbidden");
|
||||
if (!get_title_action_info(tid, &src, purl, sizeof purl,
|
||||
durl_, sizeof durl_,
|
||||
pti, sizeof pti,
|
||||
@@ -2123,6 +2159,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
patchdl_source_t src;
|
||||
int en, is_app = 0, rc;
|
||||
|
||||
if (!path_segment_safe(tid))
|
||||
return queue_text(conn, MHD_HTTP_FORBIDDEN, "forbidden");
|
||||
if (!get_title_action_info(tid, &src, purl, sizeof purl,
|
||||
durl_, sizeof durl_,
|
||||
pti, sizeof pti,
|
||||
@@ -2276,6 +2314,11 @@ patchdl_websrv_start(unsigned short port) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Lock further patchdl_scan / patchdl_scan_debug_json calls — both do a
|
||||
process-wide vnode swap that is only safe before MHD worker threads
|
||||
come up. After this point the only scan happens internally above. */
|
||||
patchdl_scan_lock();
|
||||
|
||||
/* Start background verxml fetch — joinable so patchdl_websrv_stop can wait
|
||||
for it before freeing g_titles (it reads g_titles across blocking queries). */
|
||||
g_verxml_stop = 0;
|
||||
|
||||
Reference in new issue
Block a user