Security hardening: improve file handling and socket permissions#1
Security hardening: improve file handling and socket permissions#1assisted-by-ai wants to merge 2 commits intoKicksecure:masterfrom
Conversation
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.
There was a problem hiding this comment.
Leaving this here for the sake of context; there actually could have been an issue here if privleapd's umask wasn't restrictive by default. If a file system object is created with insecure permissions, and is then chown'd/chmod'd to safety, it leaves a race window where something else could open that object with its original insecure permissions. The file descriptor won't be invalidated when the permissions change.
This isn't a problem in practice however, because privleapd already does os.umask(0o077) at the very beginning of its main() function. At most, we might want to add a comment or some documentation explaining that a server should generally do this before using the library.
| 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.
- Fix TOCTOU race in socket creation by setting umask 0177 before bind() - Fix config directory permission check to use file descriptor instead of path - Tighten state_dir and comm_dir permissions from 0755 to 0711 - Use separate PAM handles for account check vs session to prevent policy leak https://claude.ai/code/session_01W6kXMiJ5ULwNV9cega66Dx
b4c7365 to
3ca72ac
Compare
Captures reviewer conclusions from PR #1, architecture invariants, and resolved vulnerabilities so future contributors do not re-propose changes that have already been evaluated. https://claude.ai/code/session_01W6kXMiJ5ULwNV9cega66Dx
|
Accepted some of the AGENTS.md file, modifying it quite a bit and refactoring it to work with our All actual code changes are still undesirable and therefore rejected. |
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