""" JSON endpoints must not be served as text/html (reflected XSS), and templates must not build HTML by concatenating server data that contains user-controlled names (stored XSS). """ import re from pathlib import Path from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from accounts.models import UserInstance, UserSSHKey from computes.models import Compute from instances.models import Instance ROOT = Path(__file__).resolve().parent.parent.parent PAYLOAD = "" class JsonEndpointContentTypeTestCase(TestCase): def setUp(self): User = get_user_model() self.superuser = User.objects.create_superuser( username="xss_super", email="xss_super@example.com", password="password" ) self.compute = Compute.objects.create( name="xss-compute", hostname="127.0.0.1", login="root", password="", type=1 ) self.instance = Instance.objects.create( compute=self.compute, name="xss-vm", uuid="99999999-8888-7777-6666-555555555555", ) UserInstance.objects.create(instance=self.instance, user=self.superuser) UserSSHKey.objects.create( user=self.superuser, keyname="k", keypublic=f"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI {PAYLOAD}", ) self.client.force_login(self.superuser) def assertJson(self, response): self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/json") def test_guess_mac_address_reflects_vname_as_json(self): res = self.client.get( reverse("instances:guess_mac_address", args=[""]) ) self.assertJson(res) def test_random_mac_address_is_json(self): self.assertJson(self.client.get(reverse("instances:random_mac_address"))) def test_guess_clone_name_is_json(self): self.assertJson(self.client.get(reverse("instances:guess_clone_name"))) def test_sshkeys_is_json(self): self.assertJson( self.client.get(reverse("instances:sshkeys", args=[self.instance.id])) ) def test_sshkeys_plain_is_text_plain(self): res = self.client.get( reverse("instances:sshkeys", args=[self.instance.id]) + "?plain=true" ) self.assertEqual(res.status_code, 200) self.assertTrue(res["Content-Type"].startswith("text/plain")) def test_storage_volumes_is_json(self): with patch("storages.views.wvmStorage") as mock_storage: mock_storage.return_value.get_volumes.return_value = [PAYLOAD] res = self.client.get( reverse("volumes", args=[self.compute.id, "default"]) ) self.assertJson(res) def test_datasource_metadata_is_json(self): self.assertJson( self.client.get(reverse("ds_openstack_metadata", args=["latest"])) ) def test_compute_json_endpoints_are_json(self): urls = [ reverse("compute_graph", args=[self.compute.id]), reverse("machines", args=[self.compute.id, "x86_64"]), reverse("buses", args=[self.compute.id, "x86_64", "q35", "disk"]), ] with patch("computes.views.ComputeManager") as mock_mgr: for method in ("compute_graph", "get_machine_types", "get_disk_buses"): getattr(mock_mgr.return_value, method).return_value = "{}" for url in urls: with self.subTest(url=url): self.assertJson(self.client.get(url)) def test_vm_logs_is_json(self): self.assertJson(self.client.get(reverse("vm_logs", args=[self.instance.pk]))) class TemplateHtmlConcatenationTestCase(TestCase): """ The log table and the volume dropdown are filled by JavaScript from JSON that contains user-entered names. They must use DOM APIs (.text()/.val()) instead of concatenating the values into an HTML string. """ def _read(self, relative): return (ROOT / relative).read_text() def test_logs_table_does_not_concatenate_log_fields_into_html(self): src = self._read("static/js/instance_stats.js") for field in ("date", "user", "message"): self.assertNotIn(f"+row['{field}']+", src) self.assertNotIn(".html(", src) def test_volume_dropdown_does_not_concatenate_volume_names_into_html(self): src = self._read("static/js/instance_page.js") self.assertNotIn("'