From 1ea8f845d9516c51acac4efd03fffa30556f1eb1 Mon Sep 17 00:00:00 2001 From: Eduard Zateev Date: Wed, 21 Oct 2026 11:59:31 +0200 Subject: [PATCH 1/2] auth: read only the first password-file line strtok() skips leading delimiters, so an empty first line followed by a password was interpreted as that password. Stop at the first CR/LF instead, keeping an empty first line empty and making file and stdin behavior consistent. Add daemon-auth regression coverage for LF, CRLF, empty input, an empty first line, and a final line without a newline for both input sources. Fixes #1134. Signed-off-by: Eduard Zateev --- authenticate.c | 6 +- testsuite/password-file-first-line_test.py | 64 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 testsuite/password-file-first-line_test.py diff --git a/authenticate.c b/authenticate.c index 3376bb1ed..a646b16b1 100644 --- a/authenticate.c +++ b/authenticate.c @@ -271,8 +271,10 @@ static const char *getpassf(const char *filename) if (n > 0) { buffer[n] = '\0'; - if ((p = strtok(buffer, "\n\r")) != NULL) - return strdup(p); + p = buffer + strcspn(buffer, "\n\r"); + *p = '\0'; + if (buffer[0] != '\0') + return strdup(buffer); } rprintf(FERROR, "ERROR: failed to read a password from %s\n", filename); diff --git a/testsuite/password-file-first-line_test.py b/testsuite/password-file-first-line_test.py new file mode 100644 index 000000000..dc3d75539 --- /dev/null +++ b/testsuite/password-file-first-line_test.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Only the first line of --password-file is used, for files and stdin.""" + +import os +import subprocess + +from rsyncfns import ( + SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, + test_fail, write_daemon_conf, +) + +DAEMON_PORT = 12904 +USER = 'tuser' +PASSWORD = b'secretpass' + +base = SCRATCHDIR / 'password-file-first-line' +rmtree(base) +src = base / 'src' +dest = base / 'dest' +makepath(src) +(src / 'payload').write_text('regression test\n') +secrets = base / 'secrets' +secrets.write_bytes(USER.encode() + b':' + PASSWORD + b'\n') +secrets.chmod(0o600) +conf = write_daemon_conf([ + ('auth', {'path': dest, 'read only': 'no', + 'auth users': USER, 'secrets file': secrets}), +]) +url = start_test_daemon(conf, DAEMON_PORT) +userurl = url.replace('rsync://', f'rsync://{USER}@', 1) + +cases = ( + ('LF', PASSWORD + b'\nignored\n', True), + ('CRLF', PASSWORD + b'\r\nignored\r\n', True), + ('empty-file', b'', False), + ('empty-first-line', b'\n' + PASSWORD + b'\n', False), + ('no-final-newline', PASSWORD, True), +) + +for source in ('file', 'stdin'): + for name, contents, succeeds in cases: + rmtree(dest) + makepath(dest) + args = ['-a', f'{src}/', f'{userurl}auth/'] + if source == 'file': + pwfile = base / f'{name}.password' + pwfile.write_bytes(contents) + pwfile.chmod(0o600) + args.insert(1, f'--password-file={pwfile}') + stdin = subprocess.DEVNULL + else: + args.insert(1, '--password-file=-') + stdin = subprocess.PIPE + proc = subprocess.run(rsync_argv(*args), input=contents if source == 'stdin' else None, + stdin=stdin if source == 'file' else None, + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + text=False, timeout=20, + env={**os.environ, 'RSYNC_PASSWORD': 'wrong-fallback'}) + passed = proc.returncode == 0 + if passed != succeeds: + test_fail(f'{source}/{name}: expected success={succeeds}, ' + f'rc={proc.returncode}, stderr={proc.stderr!r}') + +print('password-file: first-line behavior matches for files and stdin') From 1acf5507480943a58de0b574985e7d848dfb6aa0 Mon Sep 17 00:00:00 2001 From: Eduard Zateev Date: Thu, 22 Oct 2026 16:14:11 +0200 Subject: [PATCH 2/2] tests: reject password-file fallback for empty first lines Supply the valid daemon password through RSYNC_PASSWORD in negative cases, then require exit status 1 and the password read diagnostic. Signed-off-by: Eduard Zateev --- testsuite/password-file-first-line_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/testsuite/password-file-first-line_test.py b/testsuite/password-file-first-line_test.py index dc3d75539..5af81c280 100644 --- a/testsuite/password-file-first-line_test.py +++ b/testsuite/password-file-first-line_test.py @@ -55,10 +55,16 @@ stdin=stdin if source == 'file' else None, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=False, timeout=20, - env={**os.environ, 'RSYNC_PASSWORD': 'wrong-fallback'}) + env={**os.environ, 'RSYNC_PASSWORD': + 'wrong-fallback' if succeeds else PASSWORD.decode()}) passed = proc.returncode == 0 if passed != succeeds: test_fail(f'{source}/{name}: expected success={succeeds}, ' f'rc={proc.returncode}, stderr={proc.stderr!r}') + if not succeeds and (proc.returncode != 1 + or b'failed to read a password' not in proc.stderr): + test_fail(f'{source}/{name}: expected password read failure, ' + f'rc={proc.returncode}, stderr={proc.stderr!r}') + print('password-file: first-line behavior matches for files and stdin')