|
30 | 30 |
|
31 | 31 | sys.path.append("../common")
|
32 | 32 |
|
| 33 | +import base64 |
33 | 34 | import concurrent.futures
|
34 | 35 | import json
|
35 | 36 | import os
|
@@ -2554,6 +2555,83 @@ def test_file_override(self):
|
2554 | 2555 | model_shape,
|
2555 | 2556 | )
|
2556 | 2557 |
|
| 2558 | + # Test that model load API file override can't be used to create files |
| 2559 | + # outside of any model directory. |
| 2560 | + def test_file_override_security(self): |
| 2561 | + # When using model load API, temporary model directories are created in |
| 2562 | + # a randomly generated /tmp/folderXXXXXX directory for the life of the |
| 2563 | + # model, and cleaned up on model unload. |
| 2564 | + model_basepath = "/tmp/folderXXXXXX" |
| 2565 | + if os.path.exists(model_basepath) and os.path.isdir(model_basepath): |
| 2566 | + shutil.rmtree(model_basepath) |
| 2567 | + os.makedirs(model_basepath) |
| 2568 | + |
| 2569 | + # Set file override paths that try to escape out of model directory, |
| 2570 | + # and test both pre-existing and non-existent files. |
| 2571 | + root_home_dir = "/root" |
| 2572 | + |
| 2573 | + # Relative paths |
| 2574 | + escape_dir_rel = os.path.join("..", "..", "root") |
| 2575 | + escape_dir_full = os.path.join(model_basepath, escape_dir_rel) |
| 2576 | + self.assertEqual(os.path.abspath(escape_dir_full), root_home_dir) |
| 2577 | + |
| 2578 | + new_file_rel = os.path.join(escape_dir_rel, "new_dir", "test.txt") |
| 2579 | + self.assertFalse(os.path.exists(os.path.join(model_basepath, new_file_rel))) |
| 2580 | + existing_file_rel = os.path.join(escape_dir_rel, ".bashrc") |
| 2581 | + self.assertTrue(os.path.exists(os.path.join(model_basepath, existing_file_rel))) |
| 2582 | + |
| 2583 | + # Symlinks |
| 2584 | + ## No easy way to inject symlink into generated temp model dir, so for |
| 2585 | + ## testing sake, make a fixed symlink path in /tmp. |
| 2586 | + escape_dir_symlink_rel = os.path.join("..", "escape_symlink") |
| 2587 | + escape_dir_symlink_full = "/tmp/escape_symlink" |
| 2588 | + self.assertEqual( |
| 2589 | + os.path.abspath(os.path.join(model_basepath, escape_dir_symlink_rel)), |
| 2590 | + escape_dir_symlink_full, |
| 2591 | + ) |
| 2592 | + if os.path.exists(escape_dir_symlink_full): |
| 2593 | + os.unlink(escape_dir_symlink_full) |
| 2594 | + os.symlink(root_home_dir, escape_dir_symlink_full) |
| 2595 | + self.assertTrue(os.path.abspath(escape_dir_symlink_full), root_home_dir) |
| 2596 | + |
| 2597 | + symlink_new_file_rel = os.path.join( |
| 2598 | + escape_dir_symlink_rel, "new_dir", "test.txt" |
| 2599 | + ) |
| 2600 | + self.assertFalse( |
| 2601 | + os.path.exists(os.path.join(model_basepath, symlink_new_file_rel)) |
| 2602 | + ) |
| 2603 | + symlink_existing_file_rel = os.path.join(escape_dir_symlink_rel, ".bashrc") |
| 2604 | + self.assertTrue( |
| 2605 | + os.path.exists(os.path.join(model_basepath, symlink_existing_file_rel)) |
| 2606 | + ) |
| 2607 | + |
| 2608 | + # Contents to try writing to file, though it should fail to be written |
| 2609 | + new_contents = "This shouldn't exist" |
| 2610 | + new_contents_b64 = base64.b64encode(new_contents.encode()) |
| 2611 | + |
| 2612 | + new_files = [new_file_rel, symlink_new_file_rel] |
| 2613 | + existing_files = [existing_file_rel, symlink_existing_file_rel] |
| 2614 | + all_files = new_files + existing_files |
| 2615 | + for filepath in all_files: |
| 2616 | + # minimal config to create a new model |
| 2617 | + config = json.dumps({"backend": "identity"}) |
| 2618 | + files = {f"file:{filepath}": new_contents_b64} |
| 2619 | + with httpclient.InferenceServerClient("localhost:8000") as client: |
| 2620 | + with self.assertRaisesRegex(InferenceServerException, "failed to load"): |
| 2621 | + client.load_model("new_model", config=config, files=files) |
| 2622 | + |
| 2623 | + for rel_path in new_files: |
| 2624 | + # Assert new file wasn't created |
| 2625 | + self.assertFalse(os.path.exists(os.path.join(model_basepath, rel_path))) |
| 2626 | + |
| 2627 | + for rel_path in existing_files: |
| 2628 | + # Read the existing file and make sure it's contents weren't overwritten |
| 2629 | + existing_file = os.path.join(model_basepath, rel_path) |
| 2630 | + self.assertTrue(os.path.exists(existing_file)) |
| 2631 | + with open(existing_file) as f: |
| 2632 | + contents = f.read() |
| 2633 | + self.assertNotEqual(contents, new_contents) |
| 2634 | + |
2557 | 2635 | def test_shutdown_dynamic(self):
|
2558 | 2636 | model_shape = (1, 1)
|
2559 | 2637 | input_data = np.ones(shape=(1, 1), dtype=np.float32)
|
|
0 commit comments