Skip to content

Commit 1d2d1c7

Browse files
author
reo
committed
fix: address all reviewer feedback for input validation tests
1 parent 8a44877 commit 1d2d1c7

4 files changed

Lines changed: 49 additions & 31 deletions

File tree

dream-server/extensions/services/dashboard-api/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Optional
44

5-
from pydantic import BaseModel, Field
5+
from pydantic import BaseModel, Field, validator
66

77
from config import GPU_BACKEND
88

@@ -63,7 +63,13 @@ class FullStatus(BaseModel):
6363

6464

6565
class PortCheckRequest(BaseModel):
66-
ports: list[int]
66+
ports: list[int] = Field(..., description="List of ports to check (1-65535)")
67+
68+
@validator('ports', each_item=True)
69+
def validate_port_range(cls, v):
70+
if not (1 <= v <= 65535):
71+
raise ValueError('Port must be between 1 and 65535')
72+
return v
6773

6874

6975
class PortConflict(BaseModel):

dream-server/extensions/services/dashboard-api/routers/workflows.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ async def enable_workflow(workflow_id: str, api_key: str = Depends(verify_api_ke
197197
@router.delete("/api/workflows/{workflow_id}")
198198
async def disable_workflow(workflow_id: str, api_key: str = Depends(verify_api_key)):
199199
"""Remove a workflow from n8n."""
200+
if not re.match(r'^[a-zA-Z0-9_-]+$', workflow_id):
201+
raise HTTPException(status_code=400, detail="Invalid workflow ID format")
202+
200203
n8n_workflows = await get_n8n_workflows()
201204
catalog = load_workflow_catalog()
202205
wf_info = next((wf for wf in catalog.get("workflows", []) if wf["id"] == workflow_id), None)
@@ -230,6 +233,9 @@ async def disable_workflow(workflow_id: str, api_key: str = Depends(verify_api_k
230233
@router.get("/api/workflows/{workflow_id}/executions")
231234
async def workflow_executions(workflow_id: str, limit: int = 20, api_key: str = Depends(verify_api_key)):
232235
"""Get recent executions for a workflow."""
236+
if not re.match(r'^[a-zA-Z0-9_-]+$', workflow_id):
237+
raise HTTPException(status_code=400, detail="Invalid workflow ID format")
238+
233239
n8n_workflows = await get_n8n_workflows()
234240
catalog = load_workflow_catalog()
235241
wf_info = next((wf for wf in catalog.get("workflows", []) if wf["id"] == workflow_id), None)

dream-server/extensions/services/dashboard-api/tests/test_security_validation.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_workflow_id_sql_injection_single_quote(test_client):
1818
"/api/workflows/test' OR '1'='1/enable",
1919
headers=test_client.auth_headers,
2020
)
21-
assert resp.status_code in (400, 404, 422)
21+
assert resp.status_code == 400
2222

2323

2424
def test_workflow_id_sql_injection_union(test_client):
@@ -27,7 +27,7 @@ def test_workflow_id_sql_injection_union(test_client):
2727
"/api/workflows/test' UNION SELECT * FROM users--/enable",
2828
headers=test_client.auth_headers,
2929
)
30-
assert resp.status_code in (400, 404, 422)
30+
assert resp.status_code == 400
3131

3232

3333
def test_workflow_id_command_injection_semicolon(test_client):
@@ -36,7 +36,7 @@ def test_workflow_id_command_injection_semicolon(test_client):
3636
"/api/workflows/test;rm -rf //enable",
3737
headers=test_client.auth_headers,
3838
)
39-
assert resp.status_code in (400, 404, 422)
39+
assert resp.status_code == 400
4040

4141

4242
def test_workflow_id_command_injection_pipe(test_client):
@@ -45,7 +45,7 @@ def test_workflow_id_command_injection_pipe(test_client):
4545
"/api/workflows/test|cat /etc/passwd/enable",
4646
headers=test_client.auth_headers,
4747
)
48-
assert resp.status_code in (400, 404, 422)
48+
assert resp.status_code == 400
4949

5050

5151
def test_workflow_id_command_injection_backtick(test_client):
@@ -54,7 +54,7 @@ def test_workflow_id_command_injection_backtick(test_client):
5454
"/api/workflows/test`whoami`/enable",
5555
headers=test_client.auth_headers,
5656
)
57-
assert resp.status_code in (400, 404, 422)
57+
assert resp.status_code == 400
5858

5959

6060
def test_workflow_id_null_byte_injection(test_client):
@@ -63,7 +63,7 @@ def test_workflow_id_null_byte_injection(test_client):
6363
"/api/workflows/test\x00malicious/enable",
6464
headers=test_client.auth_headers,
6565
)
66-
assert resp.status_code in (400, 404, 422)
66+
assert resp.status_code == 400
6767

6868

6969
def test_workflow_id_url_encoded_traversal(test_client):
@@ -72,7 +72,7 @@ def test_workflow_id_url_encoded_traversal(test_client):
7272
"/api/workflows/..%2F..%2Fetc%2Fpasswd/enable",
7373
headers=test_client.auth_headers,
7474
)
75-
assert resp.status_code in (400, 404, 422)
75+
assert resp.status_code == 400
7676

7777

7878
def test_workflow_id_double_encoded_traversal(test_client):
@@ -81,7 +81,7 @@ def test_workflow_id_double_encoded_traversal(test_client):
8181
"/api/workflows/..%252F..%252Fetc%252Fpasswd/enable",
8282
headers=test_client.auth_headers,
8383
)
84-
assert resp.status_code in (400, 404, 422)
84+
assert resp.status_code == 400
8585

8686

8787
# ---------------------------------------------------------------------------
@@ -95,7 +95,7 @@ def test_workflow_id_absolute_path(test_client):
9595
"/api/workflows//etc/passwd/enable",
9696
headers=test_client.auth_headers,
9797
)
98-
assert resp.status_code in (400, 404, 422)
98+
assert resp.status_code == 400
9999

100100

101101
def test_workflow_id_windows_path(test_client):
@@ -104,7 +104,7 @@ def test_workflow_id_windows_path(test_client):
104104
"/api/workflows/..\\..\\windows\\system32/enable",
105105
headers=test_client.auth_headers,
106106
)
107-
assert resp.status_code in (400, 404, 422)
107+
assert resp.status_code == 400
108108

109109

110110
def test_workflow_id_mixed_separators(test_client):
@@ -113,7 +113,7 @@ def test_workflow_id_mixed_separators(test_client):
113113
"/api/workflows/../..\\/etc/passwd/enable",
114114
headers=test_client.auth_headers,
115115
)
116-
assert resp.status_code in (400, 404, 422)
116+
assert resp.status_code == 400
117117

118118

119119
def test_workflow_id_unicode_traversal(test_client):
@@ -122,7 +122,7 @@ def test_workflow_id_unicode_traversal(test_client):
122122
"/api/workflows/\u2024\u2024/\u2024\u2024/etc/passwd/enable",
123123
headers=test_client.auth_headers,
124124
)
125-
assert resp.status_code in (400, 404, 422)
125+
assert resp.status_code == 400
126126

127127

128128
# ---------------------------------------------------------------------------
@@ -212,24 +212,30 @@ def test_preflight_ports_string_injection(test_client):
212212

213213
def test_backup_name_command_injection(test_client):
214214
"""Backup action with command injection in name → safe (validated by script path)."""
215-
# The backup name is passed as an argument to subprocess.run with list args,
215+
# The backup name is passed as an argument to asyncio.create_subprocess_exec with list args,
216216
# so command injection should not be possible. This test verifies the pattern.
217-
with patch("subprocess.run") as mock_run:
218-
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
219-
220-
# Even with malicious input, subprocess.run with list args is safe
217+
with patch("asyncio.create_subprocess_exec") as mock_exec:
218+
mock_process = AsyncMock()
219+
mock_process.returncode = 0
220+
mock_process.stdout.read.return_value = b""
221+
mock_process.stderr.read.return_value = b""
222+
mock_exec.return_value = mock_process
223+
224+
# Even with malicious input, asyncio.create_subprocess_exec with list args is safe
221225
resp = test_client.post(
222226
"/api/update",
223227
json={"action": "backup"},
224228
headers=test_client.auth_headers,
225229
)
226230

227231
# Should succeed (or fail for other reasons, but not execute injection)
228-
# The key is that subprocess.run was called with list args, not shell=True
229-
if mock_run.called:
230-
call_args = mock_run.call_args
231-
# Verify shell=True was NOT used
232-
assert call_args[1].get("shell") is not True
232+
# The key is that asyncio.create_subprocess_exec was called with list args, not shell=True
233+
if mock_exec.called:
234+
call_args = mock_exec.call_args
235+
# Verify the first argument is the script path, not a shell command
236+
assert len(call_args[0]) >= 1 # At least script path
237+
# Verify no shell=True was used (asyncio.create_subprocess_exec doesn't support shell=True anyway)
238+
assert "shell" not in call_args[1]
233239

234240

235241
def test_update_action_invalid_action(test_client):
@@ -250,5 +256,5 @@ def test_update_script_path_validation(test_client):
250256
json={"action": "check"},
251257
headers=test_client.auth_headers,
252258
)
253-
# Should fail with 501 if script doesn't exist (not 500 or command injection)
254-
assert resp.status_code in (501, 500) # 501 = not installed, 500 = other error
259+
# Should fail with 501 if script doesn't exist (not 500 crash)
260+
assert resp.status_code == 501

dream-server/extensions/services/dashboard-api/tests/test_workflows.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_workflow_enable_with_spaces(test_client):
7272
"/api/workflows/test workflow/enable",
7373
headers=test_client.auth_headers,
7474
)
75-
assert resp.status_code in (400, 404, 422)
75+
assert resp.status_code == 400
7676

7777

7878
def test_workflow_enable_with_newline(test_client):
@@ -81,7 +81,7 @@ def test_workflow_enable_with_newline(test_client):
8181
"/api/workflows/test\nworkflow/enable",
8282
headers=test_client.auth_headers,
8383
)
84-
assert resp.status_code in (400, 404, 422)
84+
assert resp.status_code == 400
8585

8686

8787
def test_workflow_enable_with_tab(test_client):
@@ -90,7 +90,7 @@ def test_workflow_enable_with_tab(test_client):
9090
"/api/workflows/test\tworkflow/enable",
9191
headers=test_client.auth_headers,
9292
)
93-
assert resp.status_code in (400, 404, 422)
93+
assert resp.status_code == 400
9494

9595

9696
def test_workflow_disable_path_traversal(test_client):
@@ -99,7 +99,7 @@ def test_workflow_disable_path_traversal(test_client):
9999
"/api/workflows/../../../etc/passwd/disable",
100100
headers=test_client.auth_headers,
101101
)
102-
assert resp.status_code in (400, 404, 422)
102+
assert resp.status_code == 400
103103

104104

105105
def test_workflow_disable_command_injection(test_client):
@@ -108,4 +108,4 @@ def test_workflow_disable_command_injection(test_client):
108108
"/api/workflows/test;rm -rf //disable",
109109
headers=test_client.auth_headers,
110110
)
111-
assert resp.status_code in (400, 404, 422)
111+
assert resp.status_code == 400

0 commit comments

Comments
 (0)