Skip to content

Trust Boundaries in Custom Code

When examining custom Python, Bash, PHP, Go, JavaScript, or Java code, do not stop at finding dangerous function names. Trace the data flow from the input source to the resources touched by privileged processing.

Identify where input from a low-privilege user is trusted during validation, storage, IPC, or processing as root. Evaluate shell=True, pickle, yaml.load, symlinks, shutil.move, archive extraction, and relative paths only after identifying who controls the input and the execution UID.

Input source
↓
Transformation and validation
↓
Storage destination or IPC
↓
Privileged process
↓
Dangerous operation
↓
Affected resource

Input sources to inspect:

  • Command-line arguments
  • Standard input
  • Environment variables
  • Configuration files
  • Log files
  • Temporary files
  • Uploaded files
  • Archive contents
  • Unix socket
  • File descriptors

Operations to inspect:

subprocess.run(...)
subprocess.Popen(...)
os.system(...)
os.execve(...)
open(...)
os.open(...)
os.pread(...)
shutil.copy(...)
shutil.move(...)
tarfile.extract(...)
tarfile.extractall(...)
zipfile.extract(...)
zipfile.extractall(...)
pickle.load(...)
yaml.load(...)
eval(...)
exec(...)
tempfile.*

The presence of these calls alone does not establish a vulnerability. Check the following:

  • Who can control the arguments.
  • Whether shell=True is used.
  • Whether paths are absolute or relative.
  • Whether the code trusts PATH.
  • File owner, mode, and ACL.
  • Whether the parent directory can be modified.
  • Whether symlinks are followed.
  • Whether validation and use involve the same inode or file descriptor.
  • Execution UID/GID.
  • The privilege difference between caller and callee.
  • Whether error handling disables a restriction.

An error response does not prove that no side effect occurred. Check callbacks, generated files, and logs even after a TypeError or 502 response.

Conditions required:
What the attacker controls:
Trust boundary:
Problematic operation:
Impact:
Why it works:
Proposed fix:
Detection and logs:
Unverified:

Include only fixes that fit the actual processing.

  • Execute with an argv array instead of a shell.
  • Restrict service or action names with an allowlist.
  • Do not store modules or checkpoints loaded as root in a directory writable by a low-privilege user.
  • Use safe temporary files.
  • Avoid symlink and TOCTOU issues with O_NOFOLLOW, owner and inode rechecks, and FD-based processing.
  • Prevent path traversal when extracting archives.
  • Do not use pickle, torch.load, or equivalent operations on files from outside the trust boundary.
  • Apply systemd hardening and least privilege, and separate read and write directories.

See Logs and Detection for what remains visible in detection logs.

Print job control file:

J line in control file
↓
job_name
↓
subprocess.Popen(..., shell=True)
↓
/bin/sh
↓
Shell under print service privileges

See Unix Sockets and File Descriptors for a diagram of FD passing.

Service name in an administrative UI:

service parameter
↓
Denylist regular expression
↓
subprocess.run("systemctl status ...", shell=True)
↓
Create symlink with service privileges
↓
Root log viewer run through sudo
↓
Read root file through a two-hop symlink

Plugin in Python run through sudo:

Plugin directory writable by a low-privilege user
↓
.pth file
↓
site.addsitedir() under root sudo
↓
Execute import line in .pth file
↓
Root-owned SUID bash

Training job checkpoint:

Processed input passes validation
↓
DataLoader
↓
Newest .pt in shared directory
↓
CheckpointLoader / torch.load
↓
pickle deserialization
↓
Command execution as root