Add drag-and-drop upload component with chunked uploads and offline support
This commit is contained in:
@@ -500,3 +500,210 @@ class TestS3StorageVerification:
|
||||
artifact = response.json()
|
||||
assert artifact["id"] == expected_hash
|
||||
assert artifact["ref_count"] == 3
|
||||
|
||||
|
||||
class TestSecurityPathTraversal:
|
||||
"""Tests for path traversal attack prevention.
|
||||
|
||||
Note: Orchard uses content-addressable storage where files are stored by
|
||||
SHA256 hash, not filename. Filenames are metadata only and never used in
|
||||
file path construction, so path traversal in filenames is not a security
|
||||
vulnerability. These tests verify the system handles unusual inputs safely.
|
||||
"""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_filename_stored_safely(
|
||||
self, integration_client, test_package
|
||||
):
|
||||
"""Test filenames with path traversal are stored safely (as metadata only)."""
|
||||
project, package = test_package
|
||||
content = b"path traversal test content"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
files = {
|
||||
"file": (
|
||||
"../../../etc/passwd",
|
||||
io.BytesIO(content),
|
||||
"application/octet-stream",
|
||||
)
|
||||
}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
data={"tag": "traversal-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["artifact_id"] == expected_hash
|
||||
s3_objects = list_s3_objects_by_hash(expected_hash)
|
||||
assert len(s3_objects) == 1
|
||||
assert ".." not in s3_objects[0]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_package_name(self, integration_client, test_project):
|
||||
"""Test package names with path traversal sequences are rejected."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages/../../../etc/passwd"
|
||||
)
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_tag_name(self, integration_client, test_package):
|
||||
"""Test tag names with path traversal are handled safely."""
|
||||
project, package = test_package
|
||||
content = b"tag traversal test"
|
||||
|
||||
files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
data={"tag": "../../../etc/passwd"},
|
||||
)
|
||||
assert response.status_code in [200, 400, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_download_path_traversal_in_ref(self, integration_client, test_package):
|
||||
"""Test download ref with path traversal is rejected."""
|
||||
project, package = test_package
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/+/../../../etc/passwd"
|
||||
)
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_package_name(self, integration_client, test_project):
|
||||
"""Test package names with path traversal sequences are rejected."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages/../../../etc/passwd"
|
||||
)
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_tag_name(self, integration_client, test_package):
|
||||
"""Test tag names with path traversal are rejected or handled safely."""
|
||||
project, package = test_package
|
||||
content = b"tag traversal test"
|
||||
|
||||
files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
data={"tag": "../../../etc/passwd"},
|
||||
)
|
||||
assert response.status_code in [200, 400, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_download_path_traversal_in_ref(self, integration_client, test_package):
|
||||
"""Test download ref with path traversal is rejected."""
|
||||
project, package = test_package
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/+/../../../etc/passwd"
|
||||
)
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
|
||||
class TestSecurityMalformedRequests:
|
||||
"""Tests for malformed request handling."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_missing_file_field(self, integration_client, test_package):
|
||||
"""Test upload without file field returns appropriate error."""
|
||||
project, package = test_package
|
||||
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
data={"tag": "no-file"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_null_bytes_in_filename(self, integration_client, test_package):
|
||||
"""Test filename with null bytes is handled safely."""
|
||||
project, package = test_package
|
||||
content = b"null byte test"
|
||||
|
||||
files = {
|
||||
"file": ("test\x00.bin", io.BytesIO(content), "application/octet-stream")
|
||||
}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
)
|
||||
assert response.status_code in [200, 400, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_very_long_filename(self, integration_client, test_package):
|
||||
"""Test very long filename is handled (truncated or rejected)."""
|
||||
project, package = test_package
|
||||
content = b"long filename test"
|
||||
long_filename = "a" * 1000 + ".bin"
|
||||
|
||||
files = {
|
||||
"file": (long_filename, io.BytesIO(content), "application/octet-stream")
|
||||
}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
)
|
||||
assert response.status_code in [200, 400, 413, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_special_characters_in_filename(
|
||||
self, integration_client, test_package
|
||||
):
|
||||
"""Test filenames with special characters are handled safely."""
|
||||
project, package = test_package
|
||||
content = b"special char test"
|
||||
|
||||
special_filenames = [
|
||||
"test<script>.bin",
|
||||
'test"quote.bin',
|
||||
"test'apostrophe.bin",
|
||||
"test;semicolon.bin",
|
||||
"test|pipe.bin",
|
||||
]
|
||||
|
||||
for filename in special_filenames:
|
||||
files = {
|
||||
"file": (filename, io.BytesIO(content), "application/octet-stream")
|
||||
}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
)
|
||||
assert response.status_code in [200, 400, 422], (
|
||||
f"Unexpected status {response.status_code} for filename: {filename}"
|
||||
)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_invalid_checksum_header_format(self, integration_client, test_package):
|
||||
"""Test invalid X-Checksum-SHA256 header format is rejected."""
|
||||
project, package = test_package
|
||||
content = b"checksum test"
|
||||
|
||||
files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
headers={"X-Checksum-SHA256": "not-a-valid-hash"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "Invalid" in response.json().get("detail", "")
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_checksum_mismatch_rejected(self, integration_client, test_package):
|
||||
"""Test upload with wrong checksum is rejected."""
|
||||
project, package = test_package
|
||||
content = b"checksum mismatch test"
|
||||
wrong_hash = "0" * 64
|
||||
|
||||
files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
headers={"X-Checksum-SHA256": wrong_hash},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert "verification failed" in response.json().get("detail", "").lower()
|
||||
|
||||
Reference in New Issue
Block a user