fix(server): reject --allow-super with --stdio, fix module host-list append
Re-review findings on the C3/C4 hardening branch: - --stdio is the SSH transport whose remote argv is composed by the client (including via --remote-option), so accepting --allow-super there let a client defeat the C3 secure default for a root receiver. Reject it at CLI parse time (standalone TCP only) and force the process-global flag off for --stdio as defense in depth. Correct the help text and README/RSYNC_COMPAT: the --stdio argv is client-composed, super stays off, and a forced command is needed if the default must hold. - daemon_conf: the per-module 'hosts allow'/'hosts deny' call sites passed module_name and replace in the wrong order, so multiple lines replaced instead of appended and the empty-value error omitted the module name. Pass (module->name, false) like the global keys; add a unit test for two per-module allow/deny lines appending. - tls: read the client CN via ASN1_STRING_to_UTF8 so an exactly-required-length name is accepted and only actual over-length CNs are rejected.
This commit is contained in:
+46
-29
@@ -37,11 +37,14 @@ static bool allow_unauthenticated;
|
||||
* root), so no super-user activity is attempted and any client --copy-as is
|
||||
* refused. Set once in main before the accept loop / stdio handler. */
|
||||
static bool server_no_super;
|
||||
/* --allow-super: standalone/--stdio opt-in that preserves the historical
|
||||
* permissive super mode for a root receiver. When false, a privileged
|
||||
* standalone receiver forces SUPER_MODE_OFF for every connection (C3), so a
|
||||
* client cannot make it create device nodes / write raw devices / apply
|
||||
* client-chosen ownership. */
|
||||
/* --allow-super: locally-launched standalone TCP opt-in that preserves the
|
||||
* historical permissive super mode for a root receiver. When false, a
|
||||
* privileged standalone receiver forces SUPER_MODE_OFF for every connection
|
||||
* (C3), so a client cannot make it create device nodes / write raw devices /
|
||||
* apply client-chosen ownership. It is REJECTED for --stdio (the SSH remote
|
||||
* argv is composed by the client, so it must never be able to opt a root
|
||||
* receiver back into super mode); the --stdio path always keeps the secure
|
||||
* default. */
|
||||
static bool server_allow_super;
|
||||
static const char* required_client_cn;
|
||||
/* --iconv CONVERT_SPEC the server was itself started with (borrowed argv
|
||||
@@ -193,17 +196,25 @@ static bool tls_client_identity_allowed(SSL* ssl) {
|
||||
X509* certificate = SSL_get1_peer_certificate(ssl);
|
||||
if (!certificate)
|
||||
return false;
|
||||
char common_name[256];
|
||||
int length = X509_NAME_get_text_by_NID(X509_get_subject_name(certificate), NID_commonName,
|
||||
common_name, sizeof(common_name));
|
||||
size_t required_length = strlen(required_client_cn);
|
||||
/* X509_NAME_get_text_by_NID truncates an over-long CN to the buffer size; a
|
||||
* returned length at the buffer bound means the CN was silently shortened, so
|
||||
* a required-name prefix could be matched by a longer CN with extra suffix.
|
||||
* Reject any result that reached the bound. */
|
||||
bool allowed = length >= 0 && (size_t)length < sizeof(common_name) - 1 &&
|
||||
(size_t)length == required_length && required_length < sizeof(common_name) &&
|
||||
credentials_secure_equal(common_name, required_client_cn, required_length);
|
||||
bool allowed = false;
|
||||
X509_NAME* subject = X509_get_subject_name(certificate);
|
||||
int index = subject ? X509_NAME_get_index_by_NID(subject, NID_commonName, -1) : -1;
|
||||
if (index >= 0) {
|
||||
X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, index);
|
||||
ASN1_STRING* data = entry ? X509_NAME_ENTRY_get_data(entry) : NULL;
|
||||
/* Convert the CN to UTF-8 to get its FULL byte length: unlike
|
||||
* X509_NAME_get_text_by_NID (which truncates an over-long CN to the buffer
|
||||
* and reports the truncated length), ASN1_STRING_to_UTF8 never truncates, so
|
||||
* an exactly-required-length CN is accepted while an over-long one cannot be
|
||||
* prefix-matched by a shorter required name. */
|
||||
unsigned char* utf8 = NULL;
|
||||
int cn_length = data ? ASN1_STRING_to_UTF8(&utf8, data) : -1;
|
||||
if (cn_length >= 0 && (size_t)cn_length == required_length)
|
||||
allowed = credentials_secure_equal((const char*)utf8, required_client_cn, required_length);
|
||||
if (utf8)
|
||||
OPENSSL_free(utf8);
|
||||
}
|
||||
X509_free(certificate);
|
||||
return allowed;
|
||||
}
|
||||
@@ -602,15 +613,17 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
if (gate_ctx)
|
||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||
}
|
||||
/* C3: a privileged (root) STANDALONE/--stdio receiver defaults to
|
||||
* SUPER_MODE_OFF. Without this a client --devices/--write-devices/--super
|
||||
* would let a root server create arbitrary device nodes and write raw devices,
|
||||
* and client-chosen ownership (--numeric-ids/--chown/--usermap/--groupmap)
|
||||
* would be applied, with no operator opt-in. The operator must pass
|
||||
* --allow-super to restore the historical permissive behavior; an
|
||||
* unprivileged receiver is unaffected (the kernel refuses the confined
|
||||
* attempts) and the daemon path keeps its per-module `client owner = yes`
|
||||
* gate. */
|
||||
/* C3: a privileged (root) STANDALONE receiver defaults to SUPER_MODE_OFF.
|
||||
* Without this a client --devices/--write-devices/--super would let a root
|
||||
* server create arbitrary device nodes and write raw devices, and
|
||||
* client-chosen ownership (--numeric-ids/--chown/--usermap/--groupmap) would
|
||||
* be applied, with no operator opt-in. The operator must pass --allow-super
|
||||
* to restore the historical permissive behavior; the flag is rejected for
|
||||
* --stdio, whose client-composed argv must never defeat this default (an
|
||||
* operator exposing `fastsync-server --stdio` over SSH needs a forced command
|
||||
* to keep the permissive behavior). An unprivileged receiver is unaffected
|
||||
* (the kernel refuses the confined attempts) and the daemon path keeps its
|
||||
* per-module `client owner = yes` gate. */
|
||||
if (g_daemon_conf == NULL && geteuid() == 0 && !server_allow_super) {
|
||||
effective.super_mode = SUPER_MODE_OFF;
|
||||
if (gate_ctx)
|
||||
@@ -1041,11 +1054,13 @@ static void print_server_usage(void) {
|
||||
printf(" --no-super Operator veto: never attempt super-user activities\n");
|
||||
printf(" (ownership, device nodes) even as root, and refuse\n");
|
||||
printf(" any client --copy-as/--super request\n");
|
||||
printf(" --allow-super Standalone/--stdio only: keep super-user activities\n");
|
||||
printf(" enabled for a root receiver. Without it a root\n");
|
||||
printf(" standalone server forces SUPER_MODE_OFF, so client\n");
|
||||
printf(" --allow-super Standalone TCP listener only: keep super-user\n");
|
||||
printf(" activities enabled for a root receiver. Without it a\n");
|
||||
printf(" root standalone server forces SUPER_MODE_OFF, so client\n");
|
||||
printf(" --devices/--write-devices/--super and ownership\n");
|
||||
printf(" requests are refused/skipped. No effect when not root\n");
|
||||
printf(" requests are refused/skipped. Never honored with\n");
|
||||
printf(" --stdio (the SSH remote argv is client-composed, so\n");
|
||||
printf(" super stays off there); no effect when not root\n");
|
||||
printf(" --iconv=LOCAL[,REMOTE] Declare this server's LOCAL charset for file-name\n");
|
||||
printf(" conversion: received names are translated to this\n");
|
||||
printf(" charset (the wire charset still comes from the\n");
|
||||
@@ -1170,7 +1185,9 @@ int main(int argc, char* argv[]) {
|
||||
trust_sender = opts.trust_sender;
|
||||
allow_unauthenticated = opts.allow_unauthenticated;
|
||||
server_no_super = opts.no_super;
|
||||
server_allow_super = opts.allow_super;
|
||||
/* --stdio rejects --allow-super at parse time; force it off here as well so
|
||||
* this process-global policy cannot be re-enabled by a future caller. */
|
||||
server_allow_super = opts.allow_super && !opts.stdio_mode;
|
||||
server_iconv_spec = opts.iconv_spec;
|
||||
signal(SIGINT, cleanup);
|
||||
signal(SIGTERM, cleanup);
|
||||
|
||||
+14
-2
@@ -267,8 +267,20 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
}
|
||||
if (opts->allow_super && opts->daemon_mode) {
|
||||
set_error(err, err_size,
|
||||
"--allow-super is for a standalone/--stdio server; daemon modules opt in per "
|
||||
"module with 'client owner = yes'");
|
||||
"--allow-super is for a locally-launched standalone TCP server; daemon modules opt "
|
||||
"in per module with 'client owner = yes'");
|
||||
return -1;
|
||||
}
|
||||
/* --stdio is the SSH transport: the remote server argv is composed by the
|
||||
* CLIENT (directly and via --remote-option), so a client could otherwise pass
|
||||
* --allow-super to a root --stdio receiver and defeat the C3 secure default.
|
||||
* Never honor it there; the super mode stays forced OFF. An operator who
|
||||
* must keep the historical permissive behavior over SSH has to launch the
|
||||
* receiver through a forced command, not via client-composed argv. */
|
||||
if (opts->allow_super && opts->stdio_mode) {
|
||||
set_error(err, err_size,
|
||||
"--allow-super is not accepted with --stdio (the remote argv is client-composed; "
|
||||
"use a forced command if the default must hold)");
|
||||
return -1;
|
||||
}
|
||||
if (opts->hash_iterations_set && opts->hash_credentials_file == NULL) {
|
||||
|
||||
+10
-7
@@ -45,13 +45,16 @@ typedef struct ServerCliOptions {
|
||||
* device-node creation) even when running as root. Applies to --stdio and
|
||||
* --daemon alike; also makes the server refuse any client --copy-as. */
|
||||
bool no_super; /* --no-super */
|
||||
/* --allow-super: standalone/--stdio only opt-in that keeps the historical
|
||||
* permissive behavior for a PRIVILEGED (root) receiver. Without it a root
|
||||
* standalone server forces SUPER_MODE_OFF, so a client --devices /
|
||||
* --write-devices / --super / ownership request cannot make it create device
|
||||
* nodes, write raw devices, or apply client-chosen ownership. Non-root
|
||||
* receivers are unaffected (the kernel refuses the confined attempts). The
|
||||
* daemon path instead uses the per-module `client owner = yes` opt-in. */
|
||||
/* --allow-super: locally-launched standalone TCP listener opt-in that keeps
|
||||
* the historical permissive behavior for a PRIVILEGED (root) receiver.
|
||||
* Without it a root standalone server forces SUPER_MODE_OFF, so a client
|
||||
* --devices / --write-devices / --super / ownership request cannot make it
|
||||
* create device nodes, write raw devices, or apply client-chosen ownership.
|
||||
* It is rejected for --stdio: that path's remote argv is composed by the
|
||||
* client (directly and via --remote-option), so it must never opt a root
|
||||
* receiver back into super mode. Non-root receivers are unaffected (the
|
||||
* kernel refuses the confined attempts). The daemon path instead uses the
|
||||
* per-module `client owner = yes` opt-in. */
|
||||
bool allow_super; /* --allow-super */
|
||||
/* --iconv=CONVERT_SPEC: the server's own LOCAL charset declaration. The
|
||||
* client's full spec rides the wire config frame anyway; when the server is
|
||||
|
||||
Reference in New Issue
Block a user