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:
Knutwurst committed 2026-06-24 20:39:43 +02:00
1 parent fcb0a5c3b0
commit 73c75ddd83
4 files changed
+74 -6

No files matched your search

+9 -4
View File
@@ -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);
+14
View File
@@ -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;
+6
View File
@@ -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
View File
@@ -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;