Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions src/backend/tests/unit/api/v1/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,221 @@
"""
response = await client.post("api/v1/validate/code", json={"code": code}, headers={"Authorization": "Bearer fake"})
assert response.status_code == status.HTTP_401_UNAUTHORIZED


# Security tests to verify dangerous operations are blocked by default
@pytest.mark.usefixtures("active_user")
async def test_validate_code_blocks_dangerous_imports_by_default(client: AsyncClient, logged_in_headers):
"""Test that dangerous imports are blocked by default."""
# Code with dangerous imports should be blocked
dangerous_code = """
import os
import subprocess
def test():
return os.getcwd()
"""
response = await client.post("api/v1/validate/code", json={"code": dangerous_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should have errors - dangerous imports are blocked
assert len(result["imports"]["errors"]) > 0 or len(result["function"]["errors"]) > 0
# Should mention that module is blocked
all_errors = result["imports"]["errors"] + result["function"]["errors"]
assert any("blocked" in str(err).lower() or "os" in str(err).lower() or "subprocess" in str(err).lower() for err in all_errors)

Check failure on line 101 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (E501)

src/backend/tests/unit/api/v1/test_validate.py:101:121: E501 Line too long (131 > 120)


@pytest.mark.usefixtures("active_user")
async def test_validate_code_blocks_dangerous_builtins_by_default(client: AsyncClient, logged_in_headers):
"""Test that dangerous builtins are blocked by default."""
# Code using dangerous builtins in default args should be blocked
# (function-definition-time execution catches this)
dangerous_code = """
def test(x=open('/etc/passwd', 'r').read()):
return x
"""
response = await client.post("api/v1/validate/code", json={"code": dangerous_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should have errors - dangerous builtins are blocked
assert len(result["function"]["errors"]) > 0
# Should mention that builtin is blocked
assert any("blocked" in str(err).lower() or "open" in str(err).lower() for err in result["function"]["errors"])


@pytest.mark.usefixtures("active_user")
async def test_validate_code_cannot_access_server_globals(client: AsyncClient, logged_in_headers):
"""Test that code cannot access server's global namespace."""
# Try to access something that would be in server globals
code_trying_to_escape = """
def test():
# Try to access parent frame or server globals
import sys
# This should execute but in isolation - can't access server's sys module state
return sys.version
"""
response = await client.post("api/v1/validate/code", json={"code": code_trying_to_escape}, headers=logged_in_headers)

Check failure on line 133 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (E501)

src/backend/tests/unit/api/v1/test_validate.py:133:121: E501 Line too long (121 > 120)
result = response.json()

Check failure on line 134 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (F841)

src/backend/tests/unit/api/v1/test_validate.py:134:5: F841 Local variable `result` is assigned to but never used
assert response.status_code == status.HTTP_200_OK
# Code executes but in isolation - sys is imported fresh, not server's sys

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix Ruff E501 / F841 in dangerous‑imports and server‑globals tests.

Ruff is (correctly) flagging:

  • Line 101: E501 line too long.
  • Line 133: E501 line too long.
  • Line 134: F841 result is assigned but never used.

You can resolve all three without changing test semantics:

@@ async def test_validate_code_blocks_dangerous_imports_by_default(client: AsyncClient, logged_in_headers):
-    all_errors = result["imports"]["errors"] + result["function"]["errors"]
-    assert any("blocked" in str(err).lower() or "os" in str(err).lower() or "subprocess" in str(err).lower() for err in all_errors)
+    all_errors = result["imports"]["errors"] + result["function"]["errors"]
+    assert any(
+        "blocked" in str(err).lower()
+        or "os" in str(err).lower()
+        or "subprocess" in str(err).lower()
+        for err in all_errors
+    )
@@ async def test_validate_code_cannot_access_server_globals(client: AsyncClient, logged_in_headers):
-    response = await client.post("api/v1/validate/code", json={"code": code_trying_to_escape}, headers=logged_in_headers)
-    result = response.json()
+    response = await client.post(
+        "api/v1/validate/code",
+        json={"code": code_trying_to_escape},
+        headers=logged_in_headers,
+    )
@@
-    assert response.status_code == status.HTTP_200_OK
-    # Code executes but in isolation - sys is imported fresh, not server's sys
+    assert response.status_code == status.HTTP_200_OK
+    # Code executes but in isolation - sys is imported fresh, not server's sys

This keeps the intent while satisfying Ruff line‑length and unused‑variable rules. As per pipeline failures.

🧰 Tools
🪛 GitHub Actions: Ruff Style Check

[error] 101-101: Ruff: E501 Line too long (131 > 120). Line exceeds maximum line length.

🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 134-134: Ruff (F841)
src/backend/tests/unit/api/v1/test_validate.py:134:5: F841 Local variable result is assigned to but never used


[failure] 133-133: Ruff (E501)
src/backend/tests/unit/api/v1/test_validate.py:133:121: E501 Line too long (121 > 120)


[failure] 101-101: Ruff (E501)
src/backend/tests/unit/api/v1/test_validate.py:101:121: E501 Line too long (131 > 120)


@pytest.mark.usefixtures("active_user")
async def test_validate_code_allows_safe_code(client: AsyncClient, logged_in_headers):
"""Test that legitimate safe code still works."""
safe_code = """
from typing import List, Optional
def process(items: List[str]) -> Optional[str]:
return items[0] if items else None
"""
response = await client.post("api/v1/validate/code", json={"code": safe_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should have no errors for safe code
assert len(result["imports"]["errors"]) == 0
assert len(result["function"]["errors"]) == 0


# Isolation verification tests
#
# IMPORTANT: These tests verify that the validate endpoint uses the sandbox.
# The actual isolation behavior is tested in src/lfx/tests/unit/custom/test_sandbox_isolation.py
# These tests just verify that dangerous code executes without crashing (proves sandbox is used)
@pytest.mark.usefixtures("active_user")
async def test_validate_code_allows_safe_imports(client: AsyncClient, logged_in_headers):
"""Test that safe imports are allowed."""
# Code with safe imports should work
safe_code = """
from typing import List, Optional
import json
import math
def test(items: List[str]) -> Optional[str]:
return json.dumps({"count": math.sqrt(len(items))})
"""
response = await client.post("api/v1/validate/code", json={"code": safe_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should execute without errors - safe imports are allowed
assert len(result["imports"]["errors"]) == 0
assert len(result["function"]["errors"]) == 0


@pytest.mark.usefixtures("active_user")
async def test_validate_code_allows_third_party_libraries(client: AsyncClient, logged_in_headers):
"""Test that third-party libraries (not in a whitelist) can be imported.

Check failure on line 184 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W293)

src/backend/tests/unit/api/v1/test_validate.py:184:1: W293 Blank line contains whitespace
Users should be able to import legitimate third-party libraries like AI libraries,
data processing libraries, etc. We only block dangerous system-level modules.
"""
# Try importing a common third-party library that wouldn't be in a whitelist
# Using 'requests' as an example - it's a legitimate library but not dangerous
# Note: This test will fail if 'requests' isn't installed, but that's okay
# The important thing is that if it IS installed, it should be allowed
third_party_code = """
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
def test():
return HAS_REQUESTS
"""
response = await client.post("api/v1/validate/code", json={"code": third_party_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should execute without errors - third-party libraries are allowed
# (unless they're in BLOCKED_MODULES like 'os', 'subprocess', etc.)
assert len(result["imports"]["errors"]) == 0
assert len(result["function"]["errors"]) == 0

Comment on lines 162 to 190
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Third‑party library test conflicts with sandbox BLOCKED_MODULES (requests is currently blocked).

test_validate_code_allows_third_party_libraries assumes that importing requests is allowed and that validation returns no errors:

# Using 'requests' as an example - it's a legitimate library but not dangerous
...
assert len(result["imports"]["errors"]) == 0
assert len(result["function"]["errors"]) == 0

However, the sandbox’s BLOCKED_MODULES set explicitly includes "requests" (and "httpx"), treating network access as dangerous. With the current sandbox, this test will fail because the import will raise a SecurityViolation, not an ImportError, and you’ll see validation errors.

You should align test expectations with the sandbox policy in one of these ways:

  • Option A (keep requests blocked): change the test to use some other third‑party library that is not in BLOCKED_MODULES (e.g., a common dependency in your stack), and update the comments to clarify that HTTP clients are intentionally blocked.
  • Option B (allow requests in validation): remove "requests" (and possibly "httpx") from BLOCKED_MODULES if you’re comfortable allowing outbound HTTP in validation, and adjust the sandbox tests accordingly.

Given the top‑level docstring in sandbox.py says that network operations are blocked by default, Option A seems more consistent with the current security model.


🏁 Script executed:

#!/bin/bash
# Double‑check the current BLOCKED_MODULES definition and where this test runs.
rg -n "BLOCKED_MODULES" -S src/lfx/src/lfx/custom/sandbox.py
rg -n "test_validate_code_allows_third_party_libraries" -S

Length of output: 424


I need to inspect the actual contents of the BLOCKED_MODULES set to verify the review comment's claim. Let me gather that information:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# View the BLOCKED_MODULES set definition (lines around 42)
sed -n '42,80p' src/lfx/src/lfx/custom/sandbox.py

Length of output: 700


Now I need to verify what happens during validation when a blocked module is encountered, and confirm the test would fail as claimed:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check the validation logic around line 156 that checks BLOCKED_MODULES
sed -n '150,175p' src/lfx/src/lfx/custom/sandbox.py

Length of output: 1285


I need to see how the validate endpoint handles SecurityViolation exceptions and what error structure gets returned:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find the validate endpoint code for the /api/v1/validate/code route
rg -n "validate/code" -A 20 -B 5 src/backend/app/api/v1/

Length of output: 63


Let me search more broadly for the validate endpoint implementation:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find the validate endpoint code by searching for the route handler
rg -n "def.*validate.*code" -A 15 src/backend/

Length of output: 297840


Blocked module ("requests") in sandbox makes this test fail — update test or sandbox policy.

BLOCKED_MODULES in src/lfx/src/lfx/custom/sandbox.py explicitly contains "requests" and "httpx" (around line 42) and the import hook raises SecurityViolation for blocked modules (check around line 156). The test src/backend/tests/unit/api/v1/test_validate.py::test_validate_code_allows_third_party_libraries (lines 181–209) expects no import/function errors but will fail under the current sandbox policy.

  • Option A (recommended): change the test to import a safe, non‑blocked third‑party library (e.g., numpy, pydantic, pandas) and update the test comments to state HTTP clients are intentionally blocked.
  • Option B: if you want validation to allow HTTP clients, remove "requests" and "httpx" from BLOCKED_MODULES and update sandbox docs/tests to reflect the policy change.
🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 184-184: Ruff (W293)
src/backend/tests/unit/api/v1/test_validate.py:184:1: W293 Blank line contains whitespace


@pytest.mark.usefixtures("active_user")
async def test_validate_code_allows_langflow_modules(client: AsyncClient, logged_in_headers):
"""Test that langflow.* modules are allowed."""
# Code importing langflow.* modules should work
langflow_code = """
import langflow
from langflow.schema import Data
def test():
return Data(data={"test": "value"})
"""
response = await client.post("api/v1/validate/code", json={"code": langflow_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should execute without errors - langflow.* modules are allowed
assert len(result["imports"]["errors"]) == 0
assert len(result["function"]["errors"]) == 0


@pytest.mark.usefixtures("active_user")
async def test_validate_code_allows_lfx_modules(client: AsyncClient, logged_in_headers):
"""Test that lfx.* modules are allowed."""
# Code importing lfx.* modules should work
lfx_code = """
import lfx
from lfx.custom import Component
def test():
return Component()
"""
response = await client.post("api/v1/validate/code", json={"code": lfx_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should execute without errors - lfx.* modules are allowed
assert len(result["imports"]["errors"]) == 0
assert len(result["function"]["errors"]) == 0


@pytest.mark.usefixtures("active_user")
async def test_validate_code_cannot_access_real_builtins(client: AsyncClient, logged_in_headers):
"""Test that code cannot escape via real __builtins__.

Check failure on line 252 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W293)

src/backend/tests/unit/api/v1/test_validate.py:252:1: W293 Blank line contains whitespace
If isolation were broken, code could access the real __builtins__ to escape.
We test this by trying to access __builtins__ directly (which should be isolated).
"""
# Try to access __builtins__ directly - if isolation is broken, this would work
code_trying_builtins_escape = """
def test():
# Try to access __builtins__ directly from globals
# If isolation is broken, this would give us the real builtins
try:
real_builtins = globals().get('__builtins__')
# If we got real builtins, we could escape - but we should get isolated version
return type(real_builtins).__name__
except Exception:
return 'isolated'
"""
response = await client.post("api/v1/validate/code", json={"code": code_trying_builtins_escape}, headers=logged_in_headers)

Check failure on line 268 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (E501)

src/backend/tests/unit/api/v1/test_validate.py:268:121: E501 Line too long (127 > 120)
result = response.json()
assert response.status_code == status.HTTP_200_OK
# Code should execute successfully - the key is that __builtins__ is isolated
# We can't easily verify it's isolated via API, but execution without errors
# combined with other isolation tests proves it works
assert len(result["function"]["errors"]) == 0 # Should execute without errors


@pytest.mark.usefixtures("active_user")
async def test_validate_code_uses_isolated_sandbox(client: AsyncClient, logged_in_headers):
"""Test that validate endpoint uses isolated sandbox.

Check failure on line 280 in src/backend/tests/unit/api/v1/test_validate.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W293)

src/backend/tests/unit/api/v1/test_validate.py:280:1: W293 Blank line contains whitespace
This test verifies that code executes in sandbox (doesn't crash).
Actual isolation behavior is tested in sandbox unit tests.
"""
# Code that sets variables - should execute without errors
# (Isolation is verified in sandbox unit tests)
code_with_variables = """
MY_VAR = "test"
def test():
return MY_VAR
"""
response = await client.post("api/v1/validate/code", json={"code": code_with_variables}, headers=logged_in_headers)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix remaining Ruff W293 / E501 issues in builtins and sandbox‑usage tests.

Ruff flags:

  • W293 at lines 252 and 280: blank lines with whitespace in docstrings.
  • E501 at line 268: client.post(...) line too long.

You can resolve them with small formatting changes:

@@ async def test_validate_code_cannot_access_real_builtins(client: AsyncClient, logged_in_headers):
-    """Test that code cannot escape via real __builtins__.
-    
-    If isolation were broken, code could access the real __builtins__ to escape.
+    """Test that code cannot escape via real __builtins__.
+    If isolation were broken, code could access the real __builtins__ to escape.
@@
-    response = await client.post("api/v1/validate/code", json={"code": code_trying_builtins_escape}, headers=logged_in_headers)
+    response = await client.post(
+        "api/v1/validate/code",
+        json={"code": code_trying_builtins_escape},
+        headers=logged_in_headers,
+    )
@@ async def test_validate_code_uses_isolated_sandbox(client: AsyncClient, logged_in_headers):
-    """Test that validate endpoint uses isolated sandbox.
-    
-    This test verifies that code executes in sandbox (doesn't crash).
+    """Test that validate endpoint uses isolated sandbox.
+    This test verifies that code executes in sandbox (doesn't crash).

This should clear the remaining Ruff warnings without changing test behavior.

🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 280-280: Ruff (W293)
src/backend/tests/unit/api/v1/test_validate.py:280:1: W293 Blank line contains whitespace


[failure] 268-268: Ruff (E501)
src/backend/tests/unit/api/v1/test_validate.py:268:121: E501 Line too long (127 > 120)


[failure] 252-252: Ruff (W293)
src/backend/tests/unit/api/v1/test_validate.py:252:1: W293 Blank line contains whitespace

🤖 Prompt for AI Agents
In src/backend/tests/unit/api/v1/test_validate.py around lines 249 to 291, fix
Ruff W293 by removing trailing whitespace/extra blank-line characters inside the
two docstrings (near lines 252 and 280) and fix E501 on the long
client.post(...) call (around line 268) by wrapping the call across multiple
lines or assigning the URL/json to a short variable and passing it, keeping the
same arguments and behavior; make only whitespace/line-break edits so tests
remain functionally unchanged.

result = response.json()
assert response.status_code == status.HTTP_200_OK
# Should execute without errors - proves sandbox is being used
assert len(result["function"]["errors"]) == 0


9 changes: 5 additions & 4 deletions src/backend/tests/unit/utils/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ def hello_world():
def test_code_with_valid_imports(self):
"""Test validation passes for code with valid imports."""
code = """
import os
import sys
from typing import List, Optional
import json
import math

def get_path():
return os.path.join(sys.path[0], "test")
def process_data(items: List[str]) -> Optional[str]:
return json.dumps({"count": math.sqrt(len(items))})
"""
result = validate_code(code)
assert result["imports"]["errors"] == []
Expand Down
Loading
Loading