|
13 | 13 | from __future__ import absolute_import |
14 | 14 |
|
15 | 15 | import os |
| 16 | +import tempfile |
16 | 17 | from pathlib import Path |
17 | 18 | from unittest.mock import patch, MagicMock, mock_open |
18 | 19 |
|
@@ -230,6 +231,60 @@ def test_download_s3_artifacts_valid_s3_path(mock_os_makedirs, mock_session): |
230 | 231 | ) |
231 | 232 |
|
232 | 233 |
|
| 234 | +@patch("sagemaker.Session") |
| 235 | +@patch("os.makedirs") |
| 236 | +def test_download_s3_artifacts_path_traversal_via_dotdot_in_key(mock_os_makedirs, mock_session): |
| 237 | + """Test that S3 keys with '..' traversal sequences are blocked.""" |
| 238 | + mock_s3_client = MagicMock() |
| 239 | + mock_s3_client.get_paginator.return_value.paginate.return_value = [ |
| 240 | + {"Contents": [{"Key": "model/../../../../etc/passwd"}]} |
| 241 | + ] |
| 242 | + mock_session.boto_session.client.return_value = mock_s3_client |
| 243 | + mock_session._get_account_id_if_default_bucket = lambda bucket: None |
| 244 | + |
| 245 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 246 | + with pytest.raises(ValueError, match="Path traversal detected"): |
| 247 | + _download_s3_artifacts("s3://my-bucket/model", tmpdir, mock_session) |
| 248 | + |
| 249 | + mock_s3_client.download_file.assert_not_called() |
| 250 | + |
| 251 | + |
| 252 | +@patch("sagemaker.Session") |
| 253 | +@patch("os.makedirs") |
| 254 | +def test_download_s3_artifacts_path_traversal_overwrite_ssh_keys(mock_os_makedirs, mock_session): |
| 255 | + """Test the attack scenario from the vulnerability report targeting SSH keys.""" |
| 256 | + mock_s3_client = MagicMock() |
| 257 | + mock_s3_client.get_paginator.return_value.paginate.return_value = [ |
| 258 | + {"Contents": [{"Key": "mlruns/exp1/model/../../../../Users/alice/.ssh/authorized_keys"}]} |
| 259 | + ] |
| 260 | + mock_session.boto_session.client.return_value = mock_s3_client |
| 261 | + mock_session._get_account_id_if_default_bucket = lambda bucket: None |
| 262 | + |
| 263 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 264 | + with pytest.raises(ValueError, match="Path traversal detected"): |
| 265 | + _download_s3_artifacts( |
| 266 | + "s3://shared-bucket/mlruns/exp1/model", tmpdir, mock_session |
| 267 | + ) |
| 268 | + |
| 269 | + mock_s3_client.download_file.assert_not_called() |
| 270 | + |
| 271 | + |
| 272 | +@patch("sagemaker.Session") |
| 273 | +def test_download_s3_artifacts_safe_keys_are_allowed(mock_session): |
| 274 | + """Test that normal S3 keys within the target directory are allowed.""" |
| 275 | + mock_s3_client = MagicMock() |
| 276 | + mock_s3_client.get_paginator.return_value.paginate.return_value = [ |
| 277 | + {"Contents": [{"Key": "model/MLmodel"}, {"Key": "model/subdir/model.pkl"}]} |
| 278 | + ] |
| 279 | + mock_session.boto_session.client.return_value = mock_s3_client |
| 280 | + mock_session._get_account_id_if_default_bucket = lambda bucket: None |
| 281 | + |
| 282 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 283 | + _download_s3_artifacts("s3://my-bucket/model", tmpdir, mock_session) |
| 284 | + |
| 285 | + assert mock_s3_client.download_file.call_count == 2 |
| 286 | + |
| 287 | + |
233 | 288 | @patch("sagemaker.image_uris.retrieve") |
234 | 289 | @patch("sagemaker.serve.model_format.mlflow.utils._cast_to_compatible_version") |
235 | 290 | @patch("sagemaker.serve.model_format.mlflow.utils._get_framework_version_from_requirements") |
|
0 commit comments