langchain - ✅(Solved) Fix Security: path traversal in FileChatMessageHistory via unsanitized session_id [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langchain-ai/langchain#36890Fetched 2026-04-20 11:58:53
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
0
Author
Timeline (top)
commented ×2labeled ×2closed ×1cross-referenced ×1

FileChatMessageHistory in langchain_core/chat_history.py constructs file paths using os.path.join(self.storage_path, self.session_id) without validating the result. When session_id comes from a user-controlled configurable (as shown in the docstring example with RunnableWithMessageHistory), an attacker can read or overwrite arbitrary files on the server.

Two bypasses:

  1. Relative traversal: session_id = "../../etc/passwd"os.path.join appends and ../ walks above storage_path
  2. Absolute path injection: session_id = "/etc/passwd"os.path.join discards the base entirely

Root Cause

FileChatMessageHistory in langchain_core/chat_history.py constructs file paths using os.path.join(self.storage_path, self.session_id) without validating the result. When session_id comes from a user-controlled configurable (as shown in the docstring example with RunnableWithMessageHistory), an attacker can read or overwrite arbitrary files on the server.

Two bypasses:

  1. Relative traversal: session_id = "../../etc/passwd"os.path.join appends and ../ walks above storage_path
  2. Absolute path injection: session_id = "/etc/passwd"os.path.join discards the base entirely

Fix Action

Fix

Add a _safe_path() helper that uses os.path.realpath() to resolve symlinks and ../, then checks the result starts with the configured storage_path:

def _safe_path(self) -> str:
    base = os.path.realpath(self.storage_path)
    resolved = os.path.realpath(
        os.path.join(self.storage_path, self.session_id)
    )
    if not resolved.startswith(base + os.sep) and resolved != base:
        raise ValueError(
            f"Invalid session_id '{self.session_id}': "
            "path resolves outside storage_path."
        )
    return resolved

All file operations (messages, add_messages, clear) should use self._safe_path() instead of the raw join.

PR fix notes

PR #36891: fix(core): guard FileChatMessageHistory against path traversal via session_id

Description (problem / solution / changelog)

Closes #36890

Summary

FileChatMessageHistory builds file paths with os.path.join(storage_path, session_id) without validating the result. When session_id is user-controlled (the standard RunnableWithMessageHistory pattern shown in the docstring), two bypasses exist:

  • Relative traversal: session_id = "../../etc/passwd" walks above storage_path
  • Absolute injection: session_id = "/etc/passwd"os.path.join discards the base entirely

Fix

Added _safe_path() helper using os.path.realpath() + prefix check. All three file operations (messages, add_messages, clear) now route through it.

def _safe_path(self) -> str:
    base = os.path.realpath(self.storage_path)
    resolved = os.path.realpath(os.path.join(self.storage_path, self.session_id))
    if not resolved.startswith(base + os.sep) and resolved != base:
        raise ValueError(
            f"Invalid session_id '{self.session_id}': "
            "path resolves outside storage_path."
        )
    return resolved

Test plan

  • FileChatMessageHistory(base, "../../etc/passwd").messages raises ValueError
  • FileChatMessageHistory(base, "/etc/passwd").messages raises ValueError
  • Normal session IDs ("user-123", "abc") continue to work

Some code in this commit was written with assistance from Claude Sonnet 4.6 (AI).

🤖 Generated with Claude Code

Changed files

  • libs/core/langchain_core/chat_history.py (modified, +19/-7)

Code Example

import os, tempfile
from langchain_core.chat_history import FileChatMessageHistory

base = tempfile.mkdtemp()

# Absolute path injection — reads /etc/passwd
h = FileChatMessageHistory("/etc/passwd")
print(h.messages)  # reads /etc/passwd

# Relative traversal — escapes storage_path
h2 = FileChatMessageHistory(os.path.join(base, "../../etc/passwd"))
print(h2.messages)

---

def _safe_path(self) -> str:
    base = os.path.realpath(self.storage_path)
    resolved = os.path.realpath(
        os.path.join(self.storage_path, self.session_id)
    )
    if not resolved.startswith(base + os.sep) and resolved != base:
        raise ValueError(
            f"Invalid session_id '{self.session_id}': "
            "path resolves outside storage_path."
        )
    return resolved
RAW_BUFFERClick to expand / collapse

Checklist

  • I am reporting a bug, not a usage question
  • I have searched for duplicate issues
  • This occurs on the latest version
  • This is a reproducible bug, not a flaky test

Package

  • langchain-core

Description

FileChatMessageHistory in langchain_core/chat_history.py constructs file paths using os.path.join(self.storage_path, self.session_id) without validating the result. When session_id comes from a user-controlled configurable (as shown in the docstring example with RunnableWithMessageHistory), an attacker can read or overwrite arbitrary files on the server.

Two bypasses:

  1. Relative traversal: session_id = "../../etc/passwd"os.path.join appends and ../ walks above storage_path
  2. Absolute path injection: session_id = "/etc/passwd"os.path.join discards the base entirely

Reproduction

import os, tempfile
from langchain_core.chat_history import FileChatMessageHistory

base = tempfile.mkdtemp()

# Absolute path injection — reads /etc/passwd
h = FileChatMessageHistory("/etc/passwd")
print(h.messages)  # reads /etc/passwd

# Relative traversal — escapes storage_path
h2 = FileChatMessageHistory(os.path.join(base, "../../etc/passwd"))
print(h2.messages)

Fix

Add a _safe_path() helper that uses os.path.realpath() to resolve symlinks and ../, then checks the result starts with the configured storage_path:

def _safe_path(self) -> str:
    base = os.path.realpath(self.storage_path)
    resolved = os.path.realpath(
        os.path.join(self.storage_path, self.session_id)
    )
    if not resolved.startswith(base + os.sep) and resolved != base:
        raise ValueError(
            f"Invalid session_id '{self.session_id}': "
            "path resolves outside storage_path."
        )
    return resolved

All file operations (messages, add_messages, clear) should use self._safe_path() instead of the raw join.

System Info

  • OS: Linux/Windows/macOS (platform-independent)
  • langchain-core: latest

extent analysis

TL;DR

To fix the vulnerability, implement a _safe_path() helper function to validate and resolve file paths before using them for file operations.

Guidance

  • Validate user-controlled input: Ensure that session_id is properly sanitized and validated to prevent malicious input.
  • Use os.path.realpath() to resolve symlinks and relative paths: This will help prevent attacks that rely on relative traversal or absolute path injection.
  • Check if the resolved path is within the configured storage_path: Raise an error if the path resolves outside the intended storage directory.
  • Update file operations to use the _safe_path() helper: Replace raw os.path.join() calls with the validated and resolved path from _safe_path().

Example

def _safe_path(self) -> str:
    base = os.path.realpath(self.storage_path)
    resolved = os.path.realpath(
        os.path.join(self.storage_path, self.session_id)
    )
    if not resolved.startswith(base + os.sep) and resolved != base:
        raise ValueError(
            f"Invalid session_id '{self.session_id}': "
            "path resolves outside storage_path."
        )
    return resolved

Notes

This fix assumes that the storage_path is a trusted and configured directory. Additional validation may be necessary depending on the specific use case and requirements.

Recommendation

Apply the workaround by implementing the _safe_path() helper function to prevent arbitrary file access vulnerabilities. This will help ensure the security and integrity of the file system.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING