test: add tests for unvalidated symlink creation in import_project

This commit is contained in:
grossmj 2026-07-29 18:48:31 +02:00
parent a225622a6e
commit b797981a65
No known key found for this signature in database
GPG Key ID: 1E7DD6DBB53FF3D7
2 changed files with 76 additions and 28 deletions

View File

@ -169,7 +169,11 @@ async def import_project(
def _create_symbolic_links(zip_file, path): def _create_symbolic_links(zip_file, path):
""" """
Materialise symlink entries, refusing any target that escapes `path`. Manually create symbolic links (if any) because ZipFile does not support it.
Refuse any target that escapes `path`.
:param zip_file: ZipFile instance
:param path: project location
""" """
path_root = os.path.realpath(path) + os.sep path_root = os.path.realpath(path) + os.sep

View File

@ -19,6 +19,8 @@ import os
import uuid import uuid
import json import json
import zipfile import zipfile
import pytest
import aiohttp
from tests.utils import asyncio_patch, AsyncioMagicMock from tests.utils import asyncio_patch, AsyncioMagicMock
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@ -117,36 +119,48 @@ async def write_file(path, z):
f.write(chunk) f.write(chunk)
async def test_import_project_containing_symlink(tmpdir, controller): @pytest.fixture
def export_project_with_symlink(tmpdir, controller):
async def _export(symlink_target):
project = Project(controller=controller, name="test")
project.dump = MagicMock()
project = Project(controller=controller, name="test") topology = {
project.dump = MagicMock() "project_id": str(uuid.uuid4()),
path = project.path "name": "test",
"auto_open": True,
"auto_start": True,
"topology": {
},
"version": "2.0.0"
}
with open(os.path.join(project.path, "project.gns3"), 'w+') as f:
json.dump(topology, f)
os.makedirs(os.path.join(project.path, "vm1", "dynamips"))
symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink")
os.symlink(symlink_target, symlink_path)
zip_path = str(tmpdir / "project.zip")
with aiozipstream.ZipFile() as z:
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
await export_project(z, project, str(tmpdir), include_images=False)
await write_file(zip_path, z)
return zip_path
return _export
async def test_import_project_containing_symlink(controller, export_project_with_symlink):
"""
Test importing a project containing a valid symlink (target inside the project directory).
"""
project_id = str(uuid.uuid4()) project_id = str(uuid.uuid4())
topology = { symlink_target = "../symlink_target"
"project_id": str(uuid.uuid4()), zip_path = await export_project_with_symlink(symlink_target)
"name": "test",
"auto_open": True,
"auto_start": True,
"topology": {
},
"version": "2.0.0"
}
with open(os.path.join(path, "project.gns3"), 'w+') as f:
json.dump(topology, f)
os.makedirs(os.path.join(path, "vm1", "dynamips"))
symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink")
symlink_target = "/tmp/anywhere"
os.symlink(symlink_target, symlink_path)
zip_path = str(tmpdir / "project.zip")
with aiozipstream.ZipFile() as z:
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
await export_project(z, project, str(tmpdir), include_images=False)
await write_file(zip_path, z)
with open(zip_path, "rb") as f: with open(zip_path, "rb") as f:
project = await import_project(controller, project_id, f) project = await import_project(controller, project_id, f)
@ -158,6 +172,36 @@ async def test_import_project_containing_symlink(tmpdir, controller):
assert os.readlink(symlink_path) == symlink_target assert os.readlink(symlink_path) == symlink_target
async def test_import_project_containing_absolute_symlink(controller, export_project_with_symlink):
"""
Test importing a project containing an absolute symlink.
This should fail because absolute symlinks are not allowed for security reasons.
"""
project_id = str(uuid.uuid4())
symlink_target = "/tmp/anywhere"
zip_path = await export_project_with_symlink(symlink_target)
with pytest.raises(aiohttp.web.HTTPConflict):
with open(zip_path, "rb") as f:
await import_project(controller, project_id, f)
async def test_import_project_containing_escaping_symlink(controller, export_project_with_symlink):
"""
Test importing a project containing a symlink that escapes the project directory.
This should fail because symlinks that escape the project directory are not allowed for security reasons.
"""
project_id = str(uuid.uuid4())
symlink_target = "../../../../symlink_target"
zip_path = await export_project_with_symlink(symlink_target)
with pytest.raises(aiohttp.web.HTTPConflict):
with open(zip_path, "rb") as f:
await import_project(controller, project_id, f)
async def test_import_upgrade(tmpdir, controller): async def test_import_upgrade(tmpdir, controller):
""" """
Topology made for previous GNS3 version are upgraded during the process Topology made for previous GNS3 version are upgraded during the process