Skip to content

Security hardening: improve file handling and socket permissions#1

Open
assisted-by-ai wants to merge 2 commits intoKicksecure:masterfrom
assisted-by-ai:claude/security-audit-5BmJL
Open

Security hardening: improve file handling and socket permissions#1
assisted-by-ai wants to merge 2 commits intoKicksecure:masterfrom
assisted-by-ai:claude/security-audit-5BmJL

Conversation

@assisted-by-ai
Copy link
Copy Markdown

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

  • Set restrictive umask (0o177) when binding UNIX sockets to prevent world-readable socket creation
  • Changed state and communication directory permissions from 0o755 to 0o711 to restrict directory listing while maintaining execute access
  • Improved socket cleanup logic to distinguish between missing files (warning) and actual errors (error level)

Configuration Parsing Improvements

  • Refactored config directory permission checking to use file descriptor-based validation instead of path-based checks
  • Added proper error handling for directory opening failures
  • Fixed config file name validation to use only the filename instead of full path
  • Ensured file descriptors are properly closed with try/finally blocks

PAM Authentication Fix

  • Separated account management PAM session from credential initialization PAM session
  • Account management now uses a dedicated pam_acct_obj instance
  • Credential initialization uses a fresh pam_obj instance with proper user context
  • This prevents credential state from being mixed between different PAM operations

Regex Pattern Refinements

  • Removed path separators (. and /) from config file regex pattern to prevent directory traversal
  • Added anchor (\Z) to uid_regex for stricter matching
  • These changes prevent malformed or malicious filenames from being processed

Notable Implementation Details

  • Socket binding now properly restores the original umask after creation
  • Configuration directory validation uses os.open() with O_RDONLY | O_DIRECTORY flags for safer file descriptor operations
  • Exception handling is more granular, distinguishing between FileNotFoundError and other exceptions for appropriate logging levels

https://claude.ai/code/session_01W6kXMiJ5ULwNV9cega66Dx

Copy link
Copy Markdown

@ArrayBolt3 ArrayBolt3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially accepted in ArrayBolt3@763ba07. Notes below.

Comment on lines -1051 to +1055
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines -1078 to +1086
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment on lines +1229 to +1237
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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

Comment on lines +1231 to +1239
uid_regex: re.Pattern[str] = re.compile(r"[0-9]+")
uid_regex: re.Pattern[str] = re.compile(r"[0-9]+\Z")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

Comment on lines +429 to +445
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,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

Comment on lines -1401 to +1417
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1442 to +1452
str(config_file), PrivleapValidateType.CONFIG_FILE
config_file.name, PrivleapValidateType.CONFIG_FILE
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

Comment on lines -1489 to +1499
PrivleapCommon.state_dir.chmod(0o755)
PrivleapCommon.state_dir.chmod(0o711)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break the ability to list the directory contents. I don't think being able to list the contents is a risk.

Comment on lines -1507 to +1517
PrivleapCommon.comm_dir.chmod(0o755)
PrivleapCommon.comm_dir.chmod(0o711)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment on lines -80 to +95
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@assisted-by-ai assisted-by-ai force-pushed the claude/security-audit-5BmJL branch from b4c7365 to 3ca72ac Compare April 27, 2026 18:25
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
@ArrayBolt3
Copy link
Copy Markdown

Accepted some of the AGENTS.md file, modifying it quite a bit and refactoring it to work with our agents/ directory structure. See ArrayBolt3@e012eca.

All actual code changes are still undesirable and therefore rejected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants