Compare commits

..
Author SHA1 Message Date
John Kennedy 52ba71ad60 test: cover env path containment 2026-07-19 23:31:19 +00:00
corridor-security[bot]andGitHub cd05fec1bc Fix: Fix Path Traversal in cli.py 2026-07-09 03:15:53 +00:00
4 changed files with 30 additions and 74 deletions
+6 -1
View File
@@ -509,7 +509,12 @@ def _resolve_env_path(
if isinstance(env_field, dict) and env_field:
return None
if isinstance(env_field, str):
env_path = (config_path.parent / env_field).resolve()
project_root = config_path.parent.resolve()
env_path = (project_root / env_field).resolve()
if not env_path.is_relative_to(project_root):
raise click.UsageError(
f"env file '{env_field}' specified in langgraph.json resolves outside the project directory."
)
if not env_path.exists():
_get_emitter().note(
f"Warning: env file '{env_field}' specified in langgraph.json not found."
@@ -227,6 +227,14 @@ class TestResolveEnvPath:
resolved = _resolve_env_path({"env": "custom.env"}, config_path)
assert resolved == env_file.resolve()
@pytest.mark.parametrize("env_field", ["../outside.env", "/etc/passwd"])
def test_env_path_outside_project_raises(self, tmp_path, env_field):
config_path = tmp_path / "langgraph.json"
config_path.touch()
with pytest.raises(click.UsageError, match="resolves outside the project"):
_resolve_env_path({"env": env_field}, config_path)
def test_missing_env_file_returns_none(self, tmp_path):
config_path = tmp_path / "langgraph.json"
config_path.touch()
+16 -29
View File
@@ -392,7 +392,7 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
def __call__(
self,
*,
resources: str | Sequence[str] | None = None,
resources: str | Sequence[str],
actions: str | Sequence[str] | None = None,
) -> Callable[
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
@@ -416,38 +416,25 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
]
):
def register(
if fn is not None:
_validate_handler(fn)
return typing.cast(
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
_register_handler(self.auth, self.resource, "*", fn),
)
def decorator(
handler: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]:
_validate_handler(handler)
if isinstance(resources, str):
resource_list = [resources]
else:
resource_list = (
list(resources) if resources is not None else [self.resource]
)
if resource_list != [self.resource]:
raise ValueError(
f"Resource-specific decorator for {self.resource!r} cannot "
f"register handlers for {resource_list!r}. Use @auth.on(...) "
"for multiple resources."
)
if isinstance(actions, str):
action_list = [actions]
else:
action_list = list(actions) if actions is not None else ["*"]
for action in action_list:
_register_handler(self.auth, self.resource, action, handler)
return handler
if fn is not None:
return register(
typing.cast(
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
fn,
)
return typing.cast(
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
_register_handler(self.auth, self.resource, "*", handler),
)
return register
# Accept keyword-only parameters for future filtering behavior; referenced to satisfy linters.
_ = resources, actions
return decorator
class _AssistantsOn(
-44
View File
@@ -1,44 +0,0 @@
import pytest
from langgraph_sdk import Auth
def _handler():
async def handler(ctx, value):
return ctx is not None and value is not None
return handler
def test_resource_decorator_registers_specific_actions():
auth = Auth()
handler = auth.on.threads(actions=["read", "search"])(_handler())
assert auth._handlers == {
("threads", "read"): [handler],
("threads", "search"): [handler],
}
def test_resource_decorator_registers_single_action():
auth = Auth()
handler = auth.on.threads(actions="read")(_handler())
assert auth._handlers == {("threads", "read"): [handler]}
def test_resource_decorator_without_actions_registers_resource_wildcard():
auth = Auth()
handler = auth.on.threads(_handler())
assert auth._handlers == {("threads", "*"): [handler]}
def test_resource_decorator_rejects_mismatched_resources():
auth = Auth()
with pytest.raises(ValueError, match=r"Use @auth\.on"):
auth.on.threads(resources="assistants", actions="read")(_handler())