Security hardening: improve file handling and socket permissions#1
Security hardening: improve file handling and socket permissions#1assisted-by-ai wants to merge 1 commit intoKicksecure:masterfrom
Conversation
- Fix TOCTOU race in socket creation by setting umask 0177 before bind() - Fix config directory permission check to use file descriptor instead of path - Fix config_file_regex to reject path traversal characters (. and /) - Fix uid_regex missing \Z end anchor allowing trailing garbage - Tighten state_dir and comm_dir permissions from 0755 to 0711 - Fix TOCTOU race in socket cleanup by removing exists() check - Use separate PAM handles for account check vs session to prevent policy leak https://claude.ai/code/session_01W6kXMiJ5ULwNV9cega66Dx
ArrayBolt3
left a comment
There was a problem hiding this comment.
Partially accepted in ArrayBolt3@763ba07. Notes below.
| self.backend_socket.bind(str(PrivleapCommon.control_path)) | ||
| old_umask = os.umask(0o177) | ||
| try: | ||
| self.backend_socket.bind(str(PrivleapCommon.control_path)) | ||
| finally: | ||
| os.umask(old_umask) |
There was a problem hiding this comment.
I don't see this as being a problem. If the socket setup fails, maybe it will leave a socket with bad permissions on disk, but the application won't use it because socket setup failed. If the socket setup succeeds, the ownership and permissions are set properly, so the umask isn't needed. privleapd already sets a restrictive umask anyway.
| self.backend_socket.bind(str(socket_path)) | ||
| old_umask = os.umask(0o177) | ||
| try: | ||
| self.backend_socket.bind(str(socket_path)) | ||
| finally: | ||
| os.umask(old_umask) |
| config_file_regex: re.Pattern[str] = re.compile(r"[-A-Za-z0-9_./]+\.conf\Z") | ||
| config_file_regex: re.Pattern[str] = re.compile(r"[-A-Za-z0-9_]+\.conf\Z") |
| uid_regex: re.Pattern[str] = re.compile(r"[0-9]+") | ||
| uid_regex: re.Pattern[str] = re.compile(r"[0-9]+\Z") |
| if socket_path.exists(): | ||
| try: | ||
| socket_path.unlink() | ||
| except Exception as e: | ||
| ## Probably just a TOCTOU issue, i.e. someone already | ||
| ## removed the socket. Most likely caused by the user | ||
| ## fiddling with things, no big deal. | ||
| logging.error( | ||
| "Destroying comm socket for account '%s', failed to " | ||
| "delete UNIX socket at '%s'", | ||
| real_user_name, | ||
| str(socket_path), | ||
| exc_info=e, | ||
| ) | ||
| else: | ||
| try: | ||
| socket_path.unlink() | ||
| except FileNotFoundError: | ||
| logging.warning( | ||
| "Destroying comm socket for account '%s', no UNIX socket " | ||
| "to delete at '%s'", | ||
| real_user_name, | ||
| str(socket_path), | ||
| ) | ||
| except Exception as e: | ||
| logging.error( | ||
| "Destroying comm socket for account '%s', failed to " | ||
| "delete UNIX socket at '%s'", | ||
| real_user_name, | ||
| str(socket_path), | ||
| exc_info=e, | ||
| ) |
| if not PrivleapCommon.check_secure_file_permissions(str(config_dir)): | ||
| try: | ||
| config_dir_fd: int = os.open( | ||
| str(config_dir), os.O_RDONLY | os.O_DIRECTORY | ||
| ) | ||
| except OSError as e: | ||
| logging.warning( | ||
| "Config directory '%s' exists but has insecure permissions, " | ||
| "ignoring all files in this directory.", | ||
| "Config directory '%s' could not be opened, skipping.", | ||
| str(config_dir), | ||
| exc_info=e, | ||
| ) | ||
| continue | ||
| try: | ||
| if not PrivleapCommon.check_secure_file_permissions(config_dir_fd): | ||
| logging.warning( | ||
| "Config directory '%s' exists but has insecure " | ||
| "permissions, ignoring all files in this directory.", | ||
| str(config_dir), | ||
| ) | ||
| continue | ||
| finally: | ||
| os.close(config_dir_fd) |
There was a problem hiding this comment.
I get the reasoning here, but disagree with it. The original code is easier to read, and while it is arguably susceptible to a TOCTOU, in practice a TOCTOU doesn't matter here. If an attacker can change the permissions from secure to insecure, swap out the directory, etc. between the check and the open, they already have the ability to write anything they want into the configuration directory and compromise the system, or set up the permissions to be correct before the swap. This patch does plug the TOCTOU, but in practice that does nothing for security.
| str(config_file), PrivleapValidateType.CONFIG_FILE | ||
| config_file.name, PrivleapValidateType.CONFIG_FILE |
| PrivleapCommon.state_dir.chmod(0o755) | ||
| PrivleapCommon.state_dir.chmod(0o711) |
There was a problem hiding this comment.
This will break the ability to list the directory contents. I don't think being able to list the contents is a risk.
| PrivleapCommon.comm_dir.chmod(0o755) | ||
| PrivleapCommon.comm_dir.chmod(0o711) |
There was a problem hiding this comment.
Ditto. (This would have the advantage of hiding the list of active users from others, but most of that info can be gotten using loginctl, so I don't think that's particularly valuable.)
| pam_obj: Any = PAM.pam() | ||
| pam_obj.start("privleapd") | ||
| pam_obj.set_item(PAM.PAM_USER, calling_user) | ||
| pam_obj.set_item(PAM.PAM_RUSER, calling_user) | ||
| pam_acct_obj: Any = PAM.pam() | ||
| pam_acct_obj.start("privleapd") | ||
| pam_acct_obj.set_item(PAM.PAM_USER, calling_user) | ||
| pam_acct_obj.set_item(PAM.PAM_RUSER, calling_user) | ||
| try: | ||
| pam_obj.acct_mgmt() | ||
| pam_acct_obj.acct_mgmt() | ||
| except PAM.error as e: | ||
| if e.args[1] == PAM.PAM_NEW_AUTHTOK_REQD: | ||
| pass | ||
| else: | ||
| sys.exit(255) | ||
|
|
||
| pam_obj: Any = PAM.pam() | ||
| pam_obj.start("privleapd") | ||
| pam_obj.set_item(PAM.PAM_USER, target_user) | ||
| pam_obj.set_item(PAM.PAM_RUSER, calling_user) |
There was a problem hiding this comment.
I don't understand why this is useful. The PAM documentation doesn't seem to hint at why this might be desirable, and sudo doesn't seem to make separate PAM sessions for this scenario, so rejecting for now.
Summary
This PR improves security and robustness across the privleap codebase by hardening file permission handling, refining socket creation with proper umask management, and fixing several edge cases in configuration parsing and PAM authentication.
Key Changes
File and Socket Permission Hardening
Configuration Parsing Improvements
PAM Authentication Fix
pam_acct_objinstancepam_objinstance with proper user contextRegex Pattern Refinements
.and/) from config file regex pattern to prevent directory traversal\Z) to uid_regex for stricter matchingNotable Implementation Details
os.open()withO_RDONLY | O_DIRECTORYflags for safer file descriptor operationsFileNotFoundErrorand other exceptions for appropriate logging levelshttps://claude.ai/code/session_01W6kXMiJ5ULwNV9cega66Dx