2406e17d2a
Introduced strict workspace boundary checks in _read_file to ensure that any resolved path remains within the workspace root, preventing path traversal. The implementation resolves the requested path against the workspace root and rejects paths that escape, returning a proper LspError to the client. Added _validate_workspace_path() static helper that canonicalises both the resolved path and the workspace root before comparing, ensuring symlinks and dot-dot segments cannot bypass the check. Added _workspace_roots dict to LspRuntime to track the workspace path per server name, populated in start_server() and consumed by get_diagnostics(), get_completions(), get_hover(), and get_definitions(). Added BDD scenarios in features/lsp_path_traversal_security.feature covering: - Path traversal via dot-dot segments - Absolute paths outside the workspace - Symlinks pointing outside the workspace - Valid paths within the workspace - Backward-compatible behaviour when workspace_root is None ISSUES CLOSED: #7215
88 lines
5.2 KiB
Gherkin
88 lines
5.2 KiB
Gherkin
Feature: LSP runtime path traversal security
|
|
As a security-conscious platform operator
|
|
I want the LSP runtime to reject file paths that escape the workspace
|
|
So that malicious LSP clients cannot read sensitive system files
|
|
|
|
# ── _validate_workspace_path unit tests ──────────────────────────
|
|
|
|
Scenario: validate_workspace_path accepts a file directly inside workspace
|
|
Given lspsec a workspace root at a temp directory
|
|
And lspsec a file "safe.py" inside the workspace
|
|
When lspsec I call validate_workspace_path with the file inside workspace
|
|
Then lspsec no error should be raised
|
|
|
|
Scenario: validate_workspace_path accepts a file in a subdirectory of workspace
|
|
Given lspsec a workspace root at a temp directory
|
|
And lspsec a file "subdir/nested.py" inside the workspace
|
|
When lspsec I call validate_workspace_path with the nested file
|
|
Then lspsec no error should be raised
|
|
|
|
Scenario: validate_workspace_path rejects a path outside the workspace
|
|
Given lspsec a workspace root at a temp directory
|
|
When lspsec I call validate_workspace_path with absolute path "/etc/passwd"
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
Scenario: validate_workspace_path rejects a path that is a sibling directory
|
|
Given lspsec a workspace root at a temp directory
|
|
And lspsec a sibling directory exists next to the workspace
|
|
When lspsec I call validate_workspace_path with a file in the sibling directory
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
Scenario: validate_workspace_path rejects the workspace root itself
|
|
Given lspsec a workspace root at a temp directory
|
|
When lspsec I call validate_workspace_path with the workspace root itself
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
# ── _read_file with workspace_root ───────────────────────────────
|
|
|
|
Scenario: read_file succeeds for a valid file inside workspace
|
|
Given lspsec a workspace root at a temp directory
|
|
And lspsec a file "hello.py" inside the workspace with content "x = 1"
|
|
When lspsec I call read_file on workspace file "hello.py" with the workspace root
|
|
Then lspsec the file content should be "x = 1"
|
|
|
|
Scenario: read_file blocks path traversal via dot-dot segments
|
|
Given lspsec a workspace root at a temp directory
|
|
And lspsec a file "hello.py" inside the workspace with content "x = 1"
|
|
When lspsec I call read_file on a dot-dot traversal path targeting etc-passwd
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
Scenario: read_file blocks an absolute path outside the workspace
|
|
Given lspsec a workspace root at a temp directory
|
|
When lspsec I call read_file on absolute path "/etc/hostname" with the workspace root
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
Scenario: read_file blocks a symlink that points outside the workspace
|
|
Given lspsec a workspace root at a temp directory
|
|
And lspsec a symlink "evil_link.py" inside the workspace pointing to "/etc/passwd"
|
|
When lspsec I call read_file on the symlink with the workspace root
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
Scenario: read_file without workspace_root skips boundary check
|
|
Given lspsec a temp file outside any workspace with content "secret"
|
|
When lspsec I call read_file on the temp file without workspace_root
|
|
Then lspsec the file content should be "secret"
|
|
|
|
# ── LspRuntime.get_diagnostics with workspace boundary ───────────
|
|
|
|
Scenario: get_diagnostics blocks path traversal when workspace is registered
|
|
Given lspsec an LspRuntime with workspace root at a temp directory
|
|
And lspsec a healthy mock server "local/pyright" registered for the workspace
|
|
When lspsec I try to get diagnostics for "local/pyright" on absolute path "/etc/passwd"
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|
|
|
|
Scenario: get_diagnostics succeeds for a valid file inside the workspace
|
|
Given lspsec an LspRuntime with workspace root at a temp directory
|
|
And lspsec a healthy mock server "local/pyright" registered for the workspace
|
|
And lspsec a file "main.py" inside the workspace with content "print('hello')"
|
|
When lspsec I get diagnostics for "local/pyright" on workspace file "main.py"
|
|
Then lspsec diagnostics should be returned as a list
|
|
|
|
# ── LspRuntime.get_completions with workspace boundary ───────────
|
|
|
|
Scenario: get_completions blocks path traversal when workspace is registered
|
|
Given lspsec an LspRuntime with workspace root at a temp directory
|
|
And lspsec a healthy mock server "local/pyright" registered for the workspace
|
|
When lspsec I try to get completions for "local/pyright" on absolute path "/etc/passwd" at line 1 column 1
|
|
Then lspsec an LspError should be raised with message containing "Path traversal attempt blocked"
|