-
Notifications
You must be signed in to change notification settings - Fork 194
Constrain server data file paths #1423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import os | ||
|
|
||
| import pytest | ||
| from fastapi import HTTPException | ||
|
|
||
| from cuopt_server import webserver | ||
| from cuopt_server.utils import settings | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def restore_data_dir(): | ||
| original_data_dir = settings.get_data_dir() | ||
| try: | ||
| yield | ||
| finally: | ||
| settings.set_data_dir(original_data_dir) | ||
|
|
||
|
|
||
| def test_validate_file_path_returns_file_if_relative_path_stays_in_data_dir( | ||
| tmp_path, | ||
| ): | ||
| data_dir = tmp_path / "data" | ||
| data_dir.mkdir() | ||
| data_file = data_dir / "input.json" | ||
| data_file.write_text("{}", encoding="utf-8") | ||
| settings.set_data_dir(str(data_dir)) | ||
|
|
||
| assert webserver.validate_file_path("input.json") == str(data_file) | ||
|
|
||
|
|
||
| def test_validate_file_path_rejects_absolute_path(tmp_path): | ||
| data_dir = tmp_path / "data" | ||
| data_dir.mkdir() | ||
| outside_file = tmp_path / "input.json" | ||
| outside_file.write_text("{}", encoding="utf-8") | ||
| settings.set_data_dir(str(data_dir)) | ||
|
|
||
| with pytest.raises(HTTPException) as exc_info: | ||
| webserver.validate_file_path(str(outside_file)) | ||
|
|
||
| assert exc_info.value.status_code == 400 | ||
| assert "relative to CUOPT_DATA_DIR" in exc_info.value.detail | ||
|
|
||
|
|
||
| def test_validate_file_path_rejects_parent_directory_escape(tmp_path): | ||
| data_dir = tmp_path / "data" | ||
| data_dir.mkdir() | ||
| outside_file = tmp_path / "input.json" | ||
| outside_file.write_text("{}", encoding="utf-8") | ||
| settings.set_data_dir(str(data_dir)) | ||
|
|
||
| with pytest.raises(HTTPException) as exc_info: | ||
| webserver.validate_file_path("../input.json") | ||
|
|
||
| assert exc_info.value.status_code == 400 | ||
| assert "stay inside CUOPT_DATA_DIR" in exc_info.value.detail | ||
|
|
||
|
|
||
| def test_validate_file_path_rejects_symlink_escape(tmp_path): | ||
| data_dir = tmp_path / "data" | ||
| data_dir.mkdir() | ||
| outside_file = tmp_path / "input.json" | ||
| outside_file.write_text("{}", encoding="utf-8") | ||
| os.symlink(outside_file, data_dir / "linked-input.json") | ||
| settings.set_data_dir(str(data_dir)) | ||
|
|
||
| with pytest.raises(HTTPException) as exc_info: | ||
| webserver.validate_file_path("linked-input.json") | ||
|
|
||
| assert exc_info.value.status_code == 400 | ||
| assert "stay inside CUOPT_DATA_DIR" in exc_info.value.detail | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -190,36 +190,36 @@ def get_output_name(resultdir, CUOPT_DATA_FILE, CUOPT_RESULT_FILE): | |
|
|
||
|
|
||
| # Validate if given data file and file path exists | ||
| def validate_file_path(CUOPT_DATA_FILE): | ||
| def validate_file_path(cuopt_data_file): | ||
| ddir = settings.get_data_dir() | ||
| try: | ||
| file_path = os.path.join(ddir, CUOPT_DATA_FILE) | ||
| if not ddir: | ||
| logging.error("cuopt data directory not set!") | ||
| # If no datadir was set but the path is relative, | ||
| # this can't work | ||
| if not CUOPT_DATA_FILE.startswith("/"): | ||
| raise ValueError( | ||
| f"cuopt server was started without data directory " | ||
| f"defined but local path {CUOPT_DATA_FILE} " | ||
| "was specified" | ||
| ) | ||
| if not os.path.exists(file_path): | ||
| logging.error(f"File path '{file_path}' doesn't exist") | ||
| msg = f"Specified path '{file_path}' does not exist" | ||
| if CUOPT_DATA_FILE.startswith("/"): | ||
| dir = os.path.dirname(CUOPT_DATA_FILE) | ||
| if not os.path.isdir(dir): | ||
| msg += f". Absolute path '{dir}' does not exist" | ||
| msg += ". Perhaps you did not intend to " | ||
| "specify an absolute path?" | ||
| raise ValueError(msg) | ||
| except Exception as e: | ||
| if not ddir: | ||
| logging.error("cuopt data directory not set!") | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="cuopt data directory not set", | ||
| ) | ||
|
|
||
| if os.path.isabs(cuopt_data_file): | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="cuopt-data-file must be relative to CUOPT_DATA_DIR", | ||
| ) | ||
|
|
||
| root = os.path.realpath(ddir) | ||
| file_path = os.path.realpath(os.path.join(root, cuopt_data_file)) | ||
| if os.path.commonpath([root, file_path]) != root: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="cuopt-data-file must stay inside CUOPT_DATA_DIR", | ||
| ) | ||
|
|
||
| if not os.path.isfile(file_path): | ||
| logging.error(f"File path '{file_path}' doesn't exist") | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="unable to read " | ||
| "optimization data from file %s, %s" % (file_path, str(e)), | ||
| detail=f"specified data file does not exist: {cuopt_data_file}", | ||
|
Comment on lines
+216
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging the resolved server path for missing files. Line 217 logs Suggested change- logging.error(f"File path '{file_path}' doesn't exist")
+ logging.error(
+ "Requested cuopt data file '%s' does not exist inside CUOPT_DATA_DIR",
+ cuopt_data_file,
+ )As per coding guidelines, 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| ) | ||
|
|
||
| return file_path | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cover the remaining new validation branches.
This suite never exercises the
CUOPT_DATA_DIR-unset branch or the new "must be a regular file" branch. A regression in either path would still leave this PR green even though both are part of the new validator behavior.Suggested tests
As per coding guidelines,
**/*test*.{cpp,py}: "Add test coverage for edge cases (empty, infeasible, unbounded, degenerate) when adding new code paths" andpython/**/tests/**: "Regression coverage for fixed bugs".🤖 Prompt for AI Agents
Source: Coding guidelines