Commands were joined into a string and passed to "su -c", so a login shell parsed every interpolated value. A library name is chosen on the server, and "sync -d /dsc/seafile/<name>" put it straight into that string: a library named 'Docs; touch /tmp/pwned; #' ran as an arbitrary command. Confirmed against the built image before and after this change. Every command is now an argument list executed by "runuser --", which execs directly with no shell. runuser -u sets the same HOME and a PATH that still finds the seaf-cli wrapper, so the daemon environment is unchanged. The password no longer needs its surrounding quotes, which also fixes passwords containing quotes, dollar signs, and backticks. Library names are untrusted, so dsc/paths.py reduces each one to a single path component and proves the result stays under the libraries directory with symlinks resolved. Empty, ".", "..", and names containing a separator are skipped with a warning; names that would collide are suffixed with their library ID rather than sharing a directory. Tests cover quotes, command substitutions, separators, newlines, absolute paths, parent traversal, Unicode, and colliding names. They run in a separate CI job so application failures stay distinguishable from packaging failures.
96 lines
3.3 KiB
Python
Executable File
96 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from dsc import SeafileClient, const
|
|
from dsc.misc import setup_uid, create_dir
|
|
from dsc.paths import plan_lib_dirs
|
|
|
|
_lg = logging.getLogger('dsc')
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO)
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("-s", "--server")
|
|
parser.add_argument("-u", "--username")
|
|
parser.add_argument("-p", "--password")
|
|
parser.add_argument("-l", "--libraries")
|
|
parser.add_argument("--uid", type=int)
|
|
parser.add_argument("--gid", type=int)
|
|
parser.add_argument("--upload-limit", type=int, default=0)
|
|
parser.add_argument("--download-limit", type=int, default=0)
|
|
parser.add_argument("--disable-verify-certificate", action="store_true")
|
|
parser.add_argument("--delete-confirm-threshold", type=int, default=500)
|
|
|
|
parser.set_defaults(
|
|
server=os.getenv("SERVER_HOST"),
|
|
username=os.getenv("USERNAME"),
|
|
password=os.getenv("PASSWORD"),
|
|
libraries=os.getenv("LIBRARY_ID"),
|
|
uid=os.getenv("SEAFILE_UID", default=1000),
|
|
gid=os.getenv("SEAFILE_GID", default=1000),
|
|
upload_limit=os.getenv("UPLOAD_LIMIT"),
|
|
download_limit=os.getenv("DOWNLOAD_LIMIT"),
|
|
disable_verify_certificate=os.getenv("DISABLE_VERIFY_CERTIFICATE") in ("true", "1", "True"),
|
|
delete_confirm_threshold=os.getenv("DELETE_CONFIRM_THRESHOLD"),
|
|
)
|
|
args = parser.parse_args()
|
|
if not args.server:
|
|
parser.error("Seafile server is not specified")
|
|
if not args.username:
|
|
parser.error("username is not specified")
|
|
if not args.password:
|
|
parser.error("password is not specified")
|
|
if not args.libraries:
|
|
parser.error("library is not specified")
|
|
|
|
setup_uid(args.uid, args.gid)
|
|
create_dir(const.DEFAULT_APP_DIR)
|
|
|
|
client = SeafileClient(args.server, args.username, args.password, const.DEFAULT_APP_DIR)
|
|
client.init_config()
|
|
client.start_daemon()
|
|
client.configure(args, check_for_daemon=False)
|
|
|
|
libs_to_sync = set()
|
|
for arg_lib in args.libraries.split(sep=":"):
|
|
lib_id = client.get_library_id(arg_lib)
|
|
if lib_id:
|
|
libs_to_sync.add(lib_id)
|
|
else:
|
|
_lg.warning("Library %s is not found on server %s", arg_lib, args.server)
|
|
|
|
# don't start to sync libraries already in sync
|
|
libs_to_sync -= client.get_local_libraries()
|
|
|
|
# check for deprecated /data directory
|
|
if os.path.isdir(const.DEPRECATED_LIBS_DIR):
|
|
_lg.warning("*** DEPRECATED DIRECTORY FOUND ***")
|
|
_lg.warning("Deprecated directory %s is found, please mount your host directory with"
|
|
" libraries to %s instead", const.DEPRECATED_LIBS_DIR, const.DEFAULT_LIBS_DIR)
|
|
libs_dir = const.DEPRECATED_LIBS_DIR
|
|
else:
|
|
libs_dir = const.DEFAULT_LIBS_DIR
|
|
|
|
# Library names come from the server, so the directory for each one is
|
|
# resolved and checked before anything is created or written.
|
|
lib_dirs = plan_lib_dirs(
|
|
{lib_id: client.remote_libraries[lib_id] for lib_id in libs_to_sync},
|
|
libs_dir,
|
|
)
|
|
for lib_id, lib_dir in lib_dirs.items():
|
|
client.sync_lib(lib_id, lib_dir)
|
|
client.watch_status()
|
|
|
|
client.stop_daemon()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|