security(shared): fix -K TOCTOU, ssh old-args quoting, TLS opts, secret-file perms, sparse dedup

- file: open -K dirlink referents via a race-safe relative O_NOFOLLOW walk
  from the authorized-root fd instead of re-opening an absolute realpath()
  result (removes the intermediate-symlink swap TOCTOU).
- transport_ssh: always single-quote the server path, including --old-args,
  so no mode can inject shell metacharacters.
- transport_tls: set SSL_OP_NO_COMPRESSION and (guarded) SSL_OP_NO_RENEGOTIATION.
- credentials: reject --password-file/--early-input with any group/other
  permission bit; chmod 0600 the affected test fixtures.
- file_store: export file_store_write_sparse() and remove the verbatim
  file.c duplicate.
This commit is contained in:
2026-09-12 15:01:48 +02:00
parent b8db810ee5
commit abad1664ba
11 changed files with 314 additions and 98 deletions
+30 -40
View File
@@ -84,32 +84,29 @@ char* ssh_build_remote_command(const char* server_path, bool old_args, char* con
const char* suffix = " --stdio";
/* Each --remote-option=OPT is appended after " --stdio" as one shell word,
escaped with the SAME single-quote boundary used for the server path. This
stays safe even in --old-args mode (which leaves the server path unquoted):
remote options are always single-quoted individually, so a value containing
shell metacharacters (; & | ` $ ()) can never break out of the quoting to
inject an unrelated remote command. Values are already validated at CLI
parse time (non-empty, no control characters); this layer only adds the
escaping boundary. */
escaped with the SAME single-quote boundary used for the server path, so a
value containing shell metacharacters (; & | ` $ ()) can never break out of
the quoting to inject an unrelated remote command. Values are already
validated at CLI parse time (non-empty, no control characters); this layer
only adds the escaping boundary. */
size_t path_len = strlen(path);
size_t suffix_len = strlen(suffix);
/* The base command (server path, quoted unless --old-args, then " --stdio"). */
size_t command_len;
if (old_args) {
if (path_len > SIZE_MAX - suffix_len - 1)
return NULL;
command_len = path_len + suffix_len + 1;
} else {
size_t quote_count = 0;
for (const char* p = path; *p; p++)
if (*p == '\'')
quote_count++;
if (path_len > SIZE_MAX - suffix_len - 4 ||
quote_count > (SIZE_MAX - path_len - suffix_len - 4) / 4)
return NULL;
command_len = path_len + quote_count * 4 + suffix_len + 4;
}
/* The base command: the server path is ALWAYS quoted as one single-quoted
shell word (remote options below reuse the same escaping), then
" --stdio". Quoting the path is the only injection-safe construction: an
unquoted path would carry shell metacharacters straight into the remote
shell command. --old-args is kept for CLI/ABI compatibility but no longer
disables that protection. */
(void)old_args;
size_t quote_count = 0;
for (const char* p = path; *p; p++)
if (*p == '\'')
quote_count++;
if (path_len > SIZE_MAX - suffix_len - 4 ||
quote_count > (SIZE_MAX - path_len - suffix_len - 4) / 4)
return NULL;
size_t command_len = path_len + quote_count * 4 + suffix_len + 4;
/* Add each remote option, escaped as one single-quoted word:
" '<body>'", i.e. 1 leading space + 1 open quote + body (len + 3 per
@@ -141,25 +138,18 @@ char* ssh_build_remote_command(const char* server_path, bool old_args, char* con
if (!command)
return NULL;
char* out = command;
if (old_args) {
memcpy(out, path, path_len);
out += path_len;
memcpy(out, suffix, suffix_len + 1);
out += suffix_len;
} else {
*out++ = '\'';
for (const char* p = path; *p; p++) {
if (*p == '\'') {
memcpy(out, "'\\''", 4);
out += 4;
} else {
*out++ = *p;
}
*out++ = '\'';
for (const char* p = path; *p; p++) {
if (*p == '\'') {
memcpy(out, "'\\''", 4);
out += 4;
} else {
*out++ = *p;
}
*out++ = '\'';
memcpy(out, suffix, suffix_len + 1);
out += suffix_len;
}
*out++ = '\'';
memcpy(out, suffix, suffix_len + 1);
out += suffix_len;
for (int i = 0; i < remote_option_count; i++) {
const char* opt = remote_options[i];
*out++ = ' ';