langchain - 💡(How to fix) Fix Bug: deprecated prompt loaders still follow relative symlinks outside the working directory [1 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#36814Fetched 2026-04-17 08:26:49
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Author
Timeline (top)
cross-referenced ×2closed ×1commented ×1labeled ×1

allow_dangerous_paths=False currently validates the user-supplied relative path string, but it still follows a relative symlink target outside the working directory when the target keeps an allowed suffix.

I reproduced the bug on current master (51e954877e, checked on 2026-04-16), and libs/core/langchain_core/prompts/loading.py has no upstream changes after langchain-core==1.2.30, so the latest stable langchain-core appears affected as well.

I confirmed the same bug class in three public deprecated loading flows:

  1. load_prompt_from_config(..., template_path="template.txt") can read a symlinked .txt target outside the cwd.
  2. load_prompt_from_config(..., examples="examples.json") can read a symlinked .json/.yaml target outside the cwd.
  3. load_prompt("prompt.json") with a relative symlink path can read a target outside the cwd.

Expected behavior: once allow_dangerous_paths=False is selected, relative prompt-loading paths should not be able to escape the working directory via symlink resolution.

Actual behavior: the loaders resolve and open the external target as long as the target keeps an accepted suffix.

A narrow fix seems to be: resolve relative paths before opening them and reject targets outside the current working directory, while leaving direct absolute-path load_prompt(...) behavior unchanged.

Error Message

No exception is raised. The repro prints: [{'input': 'TOP_SECRET', 'output': 'LEAKED'}]

Root Cause

allow_dangerous_paths=False currently validates the user-supplied relative path string, but it still follows a relative symlink target outside the working directory when the target keeps an allowed suffix.

I reproduced the bug on current master (51e954877e, checked on 2026-04-16), and libs/core/langchain_core/prompts/loading.py has no upstream changes after langchain-core==1.2.30, so the latest stable langchain-core appears affected as well.

I confirmed the same bug class in three public deprecated loading flows:

  1. load_prompt_from_config(..., template_path="template.txt") can read a symlinked .txt target outside the cwd.
  2. load_prompt_from_config(..., examples="examples.json") can read a symlinked .json/.yaml target outside the cwd.
  3. load_prompt("prompt.json") with a relative symlink path can read a target outside the cwd.

Expected behavior: once allow_dangerous_paths=False is selected, relative prompt-loading paths should not be able to escape the working directory via symlink resolution.

Actual behavior: the loaders resolve and open the external target as long as the target keeps an accepted suffix.

A narrow fix seems to be: resolve relative paths before opening them and reject targets outside the current working directory, while leaving direct absolute-path load_prompt(...) behavior unchanged.

Fix Action

Fix / Workaround

Other Dependencies

httpx: 0.28.1 jsonpatch: 1.33 packaging: 26.0 pydantic: 2.12.5 pytest: 9.0.3 pyyaml: 6.0.3 uuid-utils: 0.14.1

Code Example

import json
import os
import tempfile
from pathlib import Path

from langchain_core._api.deprecation import suppress_langchain_deprecation_warning
from langchain_core.prompts.loading import load_prompt_from_config

with tempfile.TemporaryDirectory() as td:
    root = Path(td)
    safe_dir = root / "safe"
    outside_dir = root / "outside"
    safe_dir.mkdir()
    outside_dir.mkdir()

    (outside_dir / "examples.json").write_text(
        json.dumps([{"input": "TOP_SECRET", "output": "LEAKED"}])
    )
    (safe_dir / "examples.json").symlink_to(outside_dir / "examples.json")

    config = {
        "_type": "few_shot",
        "input_variables": ["query"],
        "prefix": "Examples:",
        "example_prompt": {
            "_type": "prompt",
            "input_variables": ["input", "output"],
            "template": "{input}: {output}",
        },
        "examples": "examples.json",
        "suffix": "Query: {query}",
    }

    cwd = Path.cwd()
    os.chdir(safe_dir)
    try:
        with suppress_langchain_deprecation_warning():
            prompt = load_prompt_from_config(config, allow_dangerous_paths=False)
        print(prompt.examples)
    finally:
        os.chdir(cwd)

---

No exception is raised.
The repro prints:
[{'input': 'TOP_SECRET', 'output': 'LEAKED'}]

---

System Information
------------------
> OS:  Darwin
> OS Version:  Darwin Kernel Version 25.3.0: Wed Jan 28 20:56:42 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T8142
> Python Version:  3.14.3 (main, Feb  3 2026, 15:32:20) [Clang 17.0.0 (clang-1700.6.3.2)]

Package Information
-------------------
> langchain_core: 1.3.0a2
> langsmith: 0.7.13
> langchain_tests: 1.1.6

Other Dependencies
------------------
> httpx: 0.28.1
> jsonpatch: 1.33
> packaging: 26.0
> pydantic: 2.12.5
> pytest: 9.0.3
> pyyaml: 6.0.3
> uuid-utils: 0.14.1
RAW_BUFFERClick to expand / collapse

Related Issues / PRs

  • Follow-up hardening for #36200, #36471, and #36585
  • Related: #35195 (broader draft that appears to target the same bug class)

Reproduction Steps / Example Code (Python)

import json
import os
import tempfile
from pathlib import Path

from langchain_core._api.deprecation import suppress_langchain_deprecation_warning
from langchain_core.prompts.loading import load_prompt_from_config

with tempfile.TemporaryDirectory() as td:
    root = Path(td)
    safe_dir = root / "safe"
    outside_dir = root / "outside"
    safe_dir.mkdir()
    outside_dir.mkdir()

    (outside_dir / "examples.json").write_text(
        json.dumps([{"input": "TOP_SECRET", "output": "LEAKED"}])
    )
    (safe_dir / "examples.json").symlink_to(outside_dir / "examples.json")

    config = {
        "_type": "few_shot",
        "input_variables": ["query"],
        "prefix": "Examples:",
        "example_prompt": {
            "_type": "prompt",
            "input_variables": ["input", "output"],
            "template": "{input}: {output}",
        },
        "examples": "examples.json",
        "suffix": "Query: {query}",
    }

    cwd = Path.cwd()
    os.chdir(safe_dir)
    try:
        with suppress_langchain_deprecation_warning():
            prompt = load_prompt_from_config(config, allow_dangerous_paths=False)
        print(prompt.examples)
    finally:
        os.chdir(cwd)

Error Message and Stack Trace (if applicable)

No exception is raised.
The repro prints:
[{'input': 'TOP_SECRET', 'output': 'LEAKED'}]

Description

allow_dangerous_paths=False currently validates the user-supplied relative path string, but it still follows a relative symlink target outside the working directory when the target keeps an allowed suffix.

I reproduced the bug on current master (51e954877e, checked on 2026-04-16), and libs/core/langchain_core/prompts/loading.py has no upstream changes after langchain-core==1.2.30, so the latest stable langchain-core appears affected as well.

I confirmed the same bug class in three public deprecated loading flows:

  1. load_prompt_from_config(..., template_path="template.txt") can read a symlinked .txt target outside the cwd.
  2. load_prompt_from_config(..., examples="examples.json") can read a symlinked .json/.yaml target outside the cwd.
  3. load_prompt("prompt.json") with a relative symlink path can read a target outside the cwd.

Expected behavior: once allow_dangerous_paths=False is selected, relative prompt-loading paths should not be able to escape the working directory via symlink resolution.

Actual behavior: the loaders resolve and open the external target as long as the target keeps an accepted suffix.

A narrow fix seems to be: resolve relative paths before opening them and reject targets outside the current working directory, while leaving direct absolute-path load_prompt(...) behavior unchanged.

System Info

System Information
------------------
> OS:  Darwin
> OS Version:  Darwin Kernel Version 25.3.0: Wed Jan 28 20:56:42 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T8142
> Python Version:  3.14.3 (main, Feb  3 2026, 15:32:20) [Clang 17.0.0 (clang-1700.6.3.2)]

Package Information
-------------------
> langchain_core: 1.3.0a2
> langsmith: 0.7.13
> langchain_tests: 1.1.6

Other Dependencies
------------------
> httpx: 0.28.1
> jsonpatch: 1.33
> packaging: 26.0
> pydantic: 2.12.5
> pytest: 9.0.3
> pyyaml: 6.0.3
> uuid-utils: 0.14.1

extent analysis

TL;DR

To fix the issue, resolve relative paths before opening them and reject targets outside the current working directory when allow_dangerous_paths=False.

Guidance

  • The bug is caused by the current implementation of allow_dangerous_paths=False which only validates the user-supplied relative path string but still follows a relative symlink target outside the working directory.
  • To verify the fix, run the provided reproduction steps with the modified load_prompt_from_config function and check that it raises an error when trying to access a symlinked file outside the working directory.
  • The fix should involve modifying the load_prompt_from_config function to resolve relative paths before opening them and checking if the resolved path is within the current working directory.
  • The load_prompt function with a relative symlink path should also be updated to reject targets outside the working directory.

Example

import os
from pathlib import Path

# ...

def load_prompt_from_config(config, allow_dangerous_paths=False):
    # ...
    examples_path = Path(config["examples"])
    if not allow_dangerous_paths and not examples_path.is_absolute():
        resolved_path = (Path.cwd() / examples_path).resolve()
        if resolved_path.parent != Path.cwd():
            raise ValueError("Relative path escapes the working directory")
    # ...

Notes

The provided fix is a narrow solution and may not cover all possible scenarios. Additional testing and validation may be necessary to ensure the fix does not introduce any regressions.

Recommendation

Apply the workaround by modifying the load_prompt_from_config function to resolve relative paths and reject targets outside the working directory when allow_dangerous_paths=False. This fix addresses the specific issue described and prevents relative paths from escaping the working directory via symlink resolution.

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