fix(p8h-core): escape log paths, fail closed on identity activation, tidy server gate
- A6: escape attacker-controlled file paths and the receive root in log lines (file_receive, server, protocol DEBUG) with output_escape() - A8: identity_set_active() returns bool and fails closed when a requested usermap/groupmap cannot be deep-copied; handler refuses the connection - remove the const cast and duplicate super_mode clamp from server_module_gate via an explicit override the handler applies once - release the identity snapshot on the queue_create failure path - refactor identity_parse_copy_as to a single cleanup tail and drop the duplicated group error format specifier
This commit is contained in:
+49
-25
@@ -55,9 +55,15 @@ static CredentialStore* g_credentials = NULL;
|
||||
|
||||
/* Opaque context threaded through to the config-frame gate: the connection's
|
||||
* SSL object (NULL over plaintext) so the gate can warn when a credential
|
||||
* exchange is not encrypted. */
|
||||
* exchange is not encrypted, plus the super-mode override the gate decides on.
|
||||
* The gate never mutates the received (const) Config; it records a forced
|
||||
* SUPER_MODE_OFF here and the handler applies it exactly once after acceptance. */
|
||||
typedef struct ModuleGateContext {
|
||||
SSL* ssl;
|
||||
/* SUPER_MODE_OFF when this connection must not attempt any super-user
|
||||
activity (operator --no-super, or a daemon module without the
|
||||
`client owner = yes` opt-in); -1 when the config's own mode stands. */
|
||||
int super_mode_override;
|
||||
} ModuleGateContext;
|
||||
|
||||
/* Aggregate payload bytes the multithreaded receiver may buffer ahead of the
|
||||
@@ -182,11 +188,15 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
if (!config)
|
||||
return "missing config frame";
|
||||
/* Operator veto: --no-super forces SUPER_MODE_OFF for this connection before
|
||||
the copy-as gate is evaluated, and the caller clamps the accepted config
|
||||
again after this returns so the ownership/device gates see it too. */
|
||||
Config* effective = (Config*)config;
|
||||
if (server_no_super)
|
||||
effective->super_mode = SUPER_MODE_OFF;
|
||||
the copy-as gate is evaluated. The received config is const, so the gates
|
||||
below evaluate a shallow effective copy (only super_mode differs); the
|
||||
handler applies the recorded override to the accepted config exactly once. */
|
||||
Config effective = *config;
|
||||
if (server_no_super) {
|
||||
effective.super_mode = SUPER_MODE_OFF;
|
||||
if (gate_ctx)
|
||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||
}
|
||||
/* --copy-as (P7 Wave E, protocol 2.18.0): FastSync's safe subset forces the
|
||||
ownership of every written entry to the requested ids, which needs a
|
||||
privileged (root) receiver. An unprivileged receiver REFUSES the whole
|
||||
@@ -195,7 +205,7 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
The daemon's per-module client-chosen-ownership refusal is enforced after
|
||||
the module lookup below (it needs the module's opt-in) and covers --copy-as
|
||||
like every other ownership flag. */
|
||||
if (identity_copy_as_refused(effective)) {
|
||||
if (identity_copy_as_refused(&effective)) {
|
||||
if (geteuid() != 0)
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as requires a privileged receiver (root); refusing");
|
||||
else
|
||||
@@ -247,10 +257,10 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
standalone/SSH server has a single operator-authorized root and keeps
|
||||
honoring these. */
|
||||
if (!module->client_owner) {
|
||||
/* Ownership: refuse the whole transfer up front (a clear failure). Uses the
|
||||
original config so an explicit --super is caught even though super_mode is
|
||||
clamped to OFF below. */
|
||||
if (identity_ownership_requested(config)) {
|
||||
/* Ownership: refuse the whole transfer up front (a clear failure). Evaluated
|
||||
against the effective copy (so an operator --no-super has already
|
||||
neutralized an explicit --super), exactly as before. */
|
||||
if (identity_ownership_requested(&effective)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"daemon module '%s' refuses client-chosen ownership/super-user activities "
|
||||
"(no `client owner = yes` opt-in); refusing",
|
||||
@@ -258,13 +268,14 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
return "client-chosen ownership is not permitted by this daemon module";
|
||||
}
|
||||
/* Super-user DEVICE activities (char/block mknod and --write-devices) are
|
||||
permitted under the default AUTO mode, so without this clamp a root daemon
|
||||
would still let a non-opted module create arbitrary device nodes and write
|
||||
raw devices. Force them off for this connection: those entries are
|
||||
skipped (never mknod'ed) while an ordinary `-a` push still succeeds
|
||||
permitted under the default AUTO mode, so without this override a root
|
||||
daemon would still let a non-opted module create arbitrary device nodes
|
||||
and write raw devices. Force them off for this connection: those entries
|
||||
are skipped (never mknod'ed) while an ordinary `-a` push still succeeds
|
||||
without device nodes, matching the operator's least-privilege choice.
|
||||
The operator-level --no-super veto is already folded into this. */
|
||||
effective->super_mode = SUPER_MODE_OFF;
|
||||
if (gate_ctx)
|
||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||
}
|
||||
if (module->auth_user_count > 0) {
|
||||
/* Auth-required module (Wave B): verify the presented credentials against
|
||||
@@ -322,6 +333,7 @@ void handler(int file_descriptor) {
|
||||
protocol_session_bind(&session);
|
||||
ModuleGateContext gate_ctx;
|
||||
gate_ctx.ssl = ssl;
|
||||
gate_ctx.super_mode_override = -1;
|
||||
Config* config = config_receive_with_validate(file_descriptor, server_module_gate, &gate_ctx);
|
||||
if (config == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
|
||||
@@ -329,12 +341,13 @@ void handler(int file_descriptor) {
|
||||
protocol_session_unbind();
|
||||
return;
|
||||
}
|
||||
/* Operator --no-super veto: clamp the accepted config so every downstream
|
||||
* gate (identity_apply_ownership via privilege_super_permitted, device-node
|
||||
* creation) sees SUPER_MODE_OFF even if the gate callback did not already
|
||||
* mutate a copy of it. */
|
||||
if (server_no_super)
|
||||
config->super_mode = SUPER_MODE_OFF;
|
||||
/* Apply the super-mode veto the gate decided on (operator --no-super, or a
|
||||
* daemon module without the `client owner = yes` opt-in) exactly once, so
|
||||
* every downstream gate (identity_apply_ownership via privilege_super_permitted,
|
||||
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
||||
* received config. */
|
||||
if (gate_ctx.super_mode_override != -1)
|
||||
config->super_mode = gate_ctx.super_mode_override;
|
||||
protocol_set_8_bit_output(config->eight_bit_output);
|
||||
if (!authorized_root) {
|
||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||
@@ -417,8 +430,10 @@ void handler(int file_descriptor) {
|
||||
before anything else; without it the root must pre-exist. A failure here
|
||||
aborts the connection cleanly before any file data is exchanged. */
|
||||
if (!ensure_receive_root(config)) {
|
||||
char* escaped_root = output_escape(config->receive_root_directory, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
|
||||
config->receive_root_directory);
|
||||
escaped_root ? escaped_root : "<allocation failed>");
|
||||
free(escaped_root);
|
||||
config_delete(config);
|
||||
close(file_descriptor);
|
||||
protocol_session_unbind();
|
||||
@@ -440,8 +455,16 @@ void handler(int file_descriptor) {
|
||||
}
|
||||
/* Preserve the negotiated identity policy for the fd-relative ownership
|
||||
apply path. Each connection is its own forked process, so this
|
||||
per-process snapshot never races another connection. */
|
||||
identity_set_active(config);
|
||||
per-process snapshot never races another connection. A failed deep copy
|
||||
(allocation failure) leaves the snapshot cleared, so refuse the connection
|
||||
rather than silently applying the wrong ownership policy. */
|
||||
if (!identity_set_active(config)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to activate identity policy");
|
||||
config_delete(config);
|
||||
close(file_descriptor);
|
||||
protocol_session_unbind();
|
||||
return;
|
||||
}
|
||||
/* Persist the negotiated --keep-dirlinks policy once, here at config-accept,
|
||||
before any multithreaded receiver/writer threads are spawned, so the
|
||||
fd-walk reads a stable value during the whole transfer (and never bleeds
|
||||
@@ -485,6 +508,7 @@ void handler(int file_descriptor) {
|
||||
config_delete(config);
|
||||
close(file_descriptor);
|
||||
protocol_session_unbind();
|
||||
identity_clear_active();
|
||||
return;
|
||||
}
|
||||
PipelineContextReceiver* context =
|
||||
|
||||
@@ -350,7 +350,10 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
}
|
||||
if (is_sock) {
|
||||
/* No standard filesystem call recreates a socket; best-effort unsupported. */
|
||||
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)", file->path);
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)",
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
if (is_char || is_blk) {
|
||||
@@ -363,9 +366,11 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
so the policy does not depend on a prior identity_set_active(). Pure
|
||||
FIFO creation is unprivileged and deliberately NOT gated here. */
|
||||
if (!privilege_super_mode_permitted(config->super_mode)) {
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"skipping %s: super-user device-node creation is not permitted on this receiver",
|
||||
file->path);
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
} else if (is_fifo) {
|
||||
@@ -438,18 +443,26 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "refusing to replace existing entry with %s: %s (skipped)",
|
||||
is_fifo ? "FIFO" : "device", file->path);
|
||||
is_fifo ? "FIFO" : "device", escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
} else if (errno == EPERM || errno == EACCES) {
|
||||
/* Missing CAP_MKNOD / parent write permission: the environment cannot
|
||||
create the node, so skip instead of failing the whole run. */
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"skipping %s: cannot create %s node (%s)\n"
|
||||
" --devices/--specials node creation needs privilege (CAP_MKNOD)",
|
||||
file->path, is_fifo ? "FIFO" : "device", strerror(errno));
|
||||
escaped_path ? escaped_path : "<allocation failed>", is_fifo ? "FIFO" : "device",
|
||||
strerror(errno));
|
||||
free(escaped_path);
|
||||
} else {
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "failed to create %s %s: %s (skipped)",
|
||||
is_fifo ? "FIFO" : "device", file->path, strerror(errno));
|
||||
is_fifo ? "FIFO" : "device", escaped_path ? escaped_path : "<allocation failed>",
|
||||
strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
@@ -512,22 +525,28 @@ static FileSaveResult file_save_write_device(const char* root_directory, const F
|
||||
close(parent_fd);
|
||||
if (fd < 0) {
|
||||
free(destination);
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
const char* shown_path = escaped_path ? escaped_path : "<allocation failed>";
|
||||
if (saved_errno == ENXIO || saved_errno == EAGAIN) {
|
||||
/* A FIFO with no reader / an unreadable special: skip like every other
|
||||
unusable write-devices target instead of blocking or failing. */
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: %s not writable (%s); skipped", file->path,
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: %s not writable (%s); skipped", shown_path,
|
||||
strerror(saved_errno));
|
||||
} else {
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: cannot open %s (%s); skipped", file->path,
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: cannot open %s (%s); skipped", shown_path,
|
||||
strerror(saved_errno));
|
||||
}
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || !(S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))) {
|
||||
close(fd);
|
||||
free(destination);
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: %s is not a device node; skipped", file->path);
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: %s is not a device node; skipped",
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
bool ok = true;
|
||||
@@ -596,10 +615,12 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
open below keeps its own confinement and best-effort skip semantics). */
|
||||
if (config && config->write_devices) {
|
||||
if (!privilege_super_mode_permitted(config->super_mode)) {
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"write-devices: %s skipped: super-user activities are not permitted on this "
|
||||
"receiver",
|
||||
file->path ? file->path : "(null)");
|
||||
escaped_path ? escaped_path : "(null)");
|
||||
free(escaped_path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
return file_save_write_device(root_directory, file);
|
||||
|
||||
+67
-58
@@ -64,10 +64,10 @@ void identity_clear_active(void) {
|
||||
identity_active_reset();
|
||||
}
|
||||
|
||||
void identity_set_active(const Config* config) {
|
||||
bool identity_set_active(const Config* config) {
|
||||
identity_active_reset();
|
||||
if (!config)
|
||||
return;
|
||||
return true;
|
||||
g_identity.numeric_ids = config->numeric_ids;
|
||||
g_identity.chown_uid_set = config->chown_uid_set;
|
||||
g_identity.chown_uid = config->chown_uid;
|
||||
@@ -79,19 +79,19 @@ void identity_set_active(const Config* config) {
|
||||
g_identity.copy_as_gid = config->copy_as_gid;
|
||||
if (config->usermap_count > 0) {
|
||||
g_identity.usermap = calloc((size_t)config->usermap_count, sizeof(IdentityMap));
|
||||
if (g_identity.usermap) {
|
||||
memcpy(g_identity.usermap, config->usermap,
|
||||
(size_t)config->usermap_count * sizeof(IdentityMap));
|
||||
g_identity.usermap_count = config->usermap_count;
|
||||
}
|
||||
if (!g_identity.usermap)
|
||||
goto alloc_failed;
|
||||
memcpy(g_identity.usermap, config->usermap,
|
||||
(size_t)config->usermap_count * sizeof(IdentityMap));
|
||||
g_identity.usermap_count = config->usermap_count;
|
||||
}
|
||||
if (config->groupmap_count > 0) {
|
||||
g_identity.groupmap = calloc((size_t)config->groupmap_count, sizeof(IdentityMap));
|
||||
if (g_identity.groupmap) {
|
||||
memcpy(g_identity.groupmap, config->groupmap,
|
||||
(size_t)config->groupmap_count * sizeof(IdentityMap));
|
||||
g_identity.groupmap_count = config->groupmap_count;
|
||||
}
|
||||
if (!g_identity.groupmap)
|
||||
goto alloc_failed;
|
||||
memcpy(g_identity.groupmap, config->groupmap,
|
||||
(size_t)config->groupmap_count * sizeof(IdentityMap));
|
||||
g_identity.groupmap_count = config->groupmap_count;
|
||||
}
|
||||
g_identity.set = true;
|
||||
/* A root receiver would honor any client-supplied ownership request (a
|
||||
@@ -113,6 +113,15 @@ void identity_set_active(const Config* config) {
|
||||
"--super requested but the receiver is not privileged; super-user "
|
||||
"activities (ownership, device nodes) will be attempted but refused "
|
||||
"by the kernel and skipped per entry");
|
||||
return true;
|
||||
|
||||
alloc_failed:
|
||||
/* Never proceed with a partial (count-left-zero) map: that would silently
|
||||
apply the WRONG ownership policy. Fail closed and let the caller refuse
|
||||
the connection. */
|
||||
log_message(LOG_LEVEL_ERROR, "memory allocation failed while activating identity policy");
|
||||
identity_active_reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool privilege_super_permitted(void) {
|
||||
@@ -440,6 +449,25 @@ static bool identity_id_fits_int32(unsigned long id) {
|
||||
return id <= (unsigned long)INT32_MAX;
|
||||
}
|
||||
|
||||
/* Resolve one --copy-as id token. A '*' token means the caller's current
|
||||
* effective uid (user) or gid (group). Returns 0 on success. On failure sets
|
||||
* *overflow when a '*' id was wider than int32 so the caller can log the
|
||||
* specific message; otherwise the token was simply unresolvable. */
|
||||
static int identity_resolve_copy_as_id(const char* token, bool is_group, int32_t* out,
|
||||
bool* overflow) {
|
||||
*overflow = false;
|
||||
if (strcmp(token, "*") == 0) {
|
||||
unsigned long current = is_group ? (unsigned long)getegid() : (unsigned long)geteuid();
|
||||
if (!identity_id_fits_int32(current)) {
|
||||
*overflow = true;
|
||||
return -1;
|
||||
}
|
||||
*out = (int32_t)current;
|
||||
return 0;
|
||||
}
|
||||
return identity_resolve_token(token, is_group, out);
|
||||
}
|
||||
|
||||
int identity_parse_copy_as(Config* config, const char* value) {
|
||||
if (!config || !value || *value == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as requires USER[:GROUP]");
|
||||
@@ -465,7 +493,7 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
log_message(LOG_LEVEL_ERROR, "memory allocation failed for --copy-as");
|
||||
return -1;
|
||||
}
|
||||
char* user_token = spec;
|
||||
const char* user_token = spec;
|
||||
const char* group_token = NULL;
|
||||
char* colon = strchr(spec, ':');
|
||||
if (colon) {
|
||||
@@ -477,57 +505,39 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
* (8-bit-safe) so a control byte cannot forge a log line. */
|
||||
char* escaped_spec = output_escape(value, false);
|
||||
const char* shown = escaped_spec ? escaped_spec : "<allocation failed>";
|
||||
int ret = -1;
|
||||
|
||||
int32_t uid;
|
||||
if (*user_token == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as is missing the user (got '%s')", shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
if (strcmp(user_token, "*") == 0) {
|
||||
/* '*' means the current/root user: the client's euid. */
|
||||
if (!identity_id_fits_int32((unsigned long)geteuid())) {
|
||||
bool overflow = false;
|
||||
int32_t uid;
|
||||
if (identity_resolve_copy_as_id(user_token, false, &uid, &overflow) != 0) {
|
||||
if (overflow)
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as: current user id %lu exceeds INT32_MAX",
|
||||
(unsigned long)geteuid());
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
uid = (int32_t)geteuid();
|
||||
} else if (identity_resolve_token(user_token, false, &uid) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as could not resolve user (use a name that exists on the "
|
||||
"source, '*', or @N): %s",
|
||||
shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
else
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as could not resolve user (use a name that exists on the "
|
||||
"source, '*', or @N): %s",
|
||||
shown);
|
||||
goto done;
|
||||
}
|
||||
|
||||
int32_t gid;
|
||||
if (group_token) {
|
||||
if (*group_token == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as group is empty (got '%s')", shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
if (strcmp(group_token, "*") == 0) {
|
||||
if (!identity_id_fits_int32((unsigned long)getegid())) {
|
||||
if (identity_resolve_copy_as_id(group_token, true, &gid, &overflow) != 0) {
|
||||
if (overflow)
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as: current group id %lu exceeds INT32_MAX",
|
||||
(unsigned long)getegid());
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
gid = (int32_t)getegid();
|
||||
} else if (identity_resolve_token(group_token, true, &gid) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as could not resolve group (got '%s'): %s", shown,
|
||||
shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
else
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as could not resolve group (got '%s')", shown);
|
||||
goto done;
|
||||
}
|
||||
} else {
|
||||
/* Group omitted: use the user's primary gid. A numeric id with no local
|
||||
@@ -539,9 +549,7 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as: primary group id %lu for the requested user exceeds INT32_MAX",
|
||||
(unsigned long)pw->pw_gid);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
gid = (int32_t)pw->pw_gid;
|
||||
} else {
|
||||
@@ -553,12 +561,8 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
* identity_resolve_token. */
|
||||
if (uid < 0 || gid < 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as resolved id does not fit in int32 (got '%s')", shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
|
||||
config->copy_as_set = true;
|
||||
config->copy_as_uid = uid;
|
||||
@@ -566,7 +570,12 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
/* Ownership application needs the metadata path (the source uid/gid must be
|
||||
* transmitted); imply it exactly like --chown/--usermap/--groupmap. */
|
||||
config->use_metadata = true;
|
||||
return 0;
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ---- Receiver-side ownership application ---- */
|
||||
|
||||
@@ -67,8 +67,13 @@ bool identity_copy_as_active(void);
|
||||
/* Receiver-side snapshot of the negotiated identity config. The server calls
|
||||
* identity_set_active() once per connection (before any file write) using the
|
||||
* config received over the wire; the snapshot is a deep copy so the caller may
|
||||
* free its Config immediately. identity_clear_active() releases it. */
|
||||
void identity_set_active(const Config* config);
|
||||
* free its Config immediately. identity_clear_active() releases it.
|
||||
*
|
||||
* Returns true on success. On an allocation failure while deep-copying a
|
||||
* requested usermap/groupmap it logs a LOG_LEVEL_ERROR, leaves the snapshot
|
||||
* cleared (never a partial/wrong policy) and returns false; the caller must
|
||||
* refuse the connection. */
|
||||
bool identity_set_active(const Config* config);
|
||||
void identity_clear_active(void);
|
||||
|
||||
/* True when any ownership-affecting identity option is present in the active
|
||||
|
||||
@@ -459,10 +459,14 @@ static char* protocol_receive_str_impl(ProtocolSession* session, bool redact) {
|
||||
return NULL;
|
||||
}
|
||||
data[size] = '\0';
|
||||
if (redact)
|
||||
if (redact) {
|
||||
log_debug_message(LOG_DEBUG_PROTO, "Received String: <redacted>");
|
||||
else
|
||||
log_debug_message(LOG_DEBUG_PROTO, "Received String: %s", data);
|
||||
} else {
|
||||
char* escaped_data = output_escape(data, log_get_8_bit_output());
|
||||
log_debug_message(LOG_DEBUG_PROTO, "Received String: %s",
|
||||
escaped_data ? escaped_data : "<allocation failed>");
|
||||
free(escaped_data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -1883,13 +1883,13 @@ static void test_privilege_super_permitted_modes() {
|
||||
Config* c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->super_mode = SUPER_MODE_OFF;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_FALSE(privilege_super_permitted());
|
||||
c->super_mode = SUPER_MODE_ON;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(privilege_super_permitted());
|
||||
c->super_mode = SUPER_MODE_AUTO;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(privilege_super_permitted());
|
||||
config_delete(c);
|
||||
|
||||
@@ -1952,10 +1952,10 @@ static void test_super_does_not_imply_numeric() {
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->super_mode = SUPER_MODE_ON;
|
||||
c->use_metadata = true;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_FALSE(identity_active_enabled());
|
||||
c->numeric_ids = true;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(identity_active_enabled());
|
||||
identity_clear_active();
|
||||
config_delete(c);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "config.h"
|
||||
#include "delta.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include "utils.h"
|
||||
@@ -724,7 +725,44 @@ static void test_receiver_pending_commits_missing_args() {
|
||||
free(root);
|
||||
}
|
||||
|
||||
/* A6: an attacker-controlled file path appearing in a log line must be escaped
|
||||
so a control byte cannot forge a second log record. The socket special-node
|
||||
branch logs file->path before touching the filesystem, making it a cheap way
|
||||
to exercise an escaped site. The captured line must contain the escaped path
|
||||
(`\#012` for the newline), never the raw control byte. */
|
||||
static void test_special_socket_path_log_escaped() {
|
||||
set_log_level(LOG_LEVEL_WARNING);
|
||||
log_set_8_bit_output(false);
|
||||
|
||||
FILE* capture = tmpfile();
|
||||
EXPECT_NOT_NULL(capture);
|
||||
log_set_file(capture);
|
||||
|
||||
File* file = file_create("evil\npath");
|
||||
EXPECT_NOT_NULL(file);
|
||||
file->is_special = true;
|
||||
file->metadata = calloc(1, sizeof(FileMetadata));
|
||||
EXPECT_NOT_NULL(file->metadata);
|
||||
file->metadata->mode = S_IFSOCK | 0644;
|
||||
|
||||
FileSaveResult result = file_save_to_disk_full("/tmp/dst", file, NULL);
|
||||
EXPECT_EQ_INT(result, FILE_SAVE_SKIPPED);
|
||||
|
||||
fflush(capture);
|
||||
rewind(capture);
|
||||
char output[512] = {0};
|
||||
size_t length = fread(output, 1, sizeof(output) - 1, capture);
|
||||
output[length] = '\0';
|
||||
|
||||
log_set_file(NULL);
|
||||
fclose(capture);
|
||||
file_destroy(file);
|
||||
|
||||
EXPECT_NOT_NULL(strstr(output, "socket not recreated: evil\\#012path"));
|
||||
}
|
||||
|
||||
void test_server() {
|
||||
test_special_socket_path_log_escaped();
|
||||
if (!is_running_under_valgrind()) {
|
||||
test_receive_files_finished();
|
||||
test_receive_files_single_file();
|
||||
|
||||
+4
-4
@@ -311,7 +311,7 @@ static void test_fake_super_owner_gate() {
|
||||
|
||||
/* --no-super: the owner leg is skipped even as root. */
|
||||
c->super_mode = SUPER_MODE_OFF;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd));
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
@@ -320,7 +320,7 @@ static void test_fake_super_owner_gate() {
|
||||
|
||||
/* AUTO with an identity policy: the recorded source owner is applied. */
|
||||
c->super_mode = SUPER_MODE_AUTO;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 12345);
|
||||
@@ -331,7 +331,7 @@ static void test_fake_super_owner_gate() {
|
||||
EXPECT_EQ_INT(fchown(fd, 0, 0), 0);
|
||||
c->numeric_ids = false;
|
||||
c->super_mode = SUPER_MODE_ON;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 0);
|
||||
@@ -342,7 +342,7 @@ static void test_fake_super_owner_gate() {
|
||||
c->copy_as_set = true;
|
||||
c->copy_as_uid = 777;
|
||||
c->copy_as_gid = 778;
|
||||
identity_set_active(c);
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 0);
|
||||
|
||||
Reference in New Issue
Block a user