Skip to content

Commit ed5eb49

Browse files
committed
[Fix] Ensure malformed UUID URLs return 404 #682
Applied strict regex across URLs and updated tests which comply while adding regression test for 404 on malformed URLs. Fixes #682
1 parent 691bae8 commit ed5eb49

File tree

6 files changed

+115
-34
lines changed

6 files changed

+115
-34
lines changed

openwisp_controller/config/admin.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949

5050
logger = logging.getLogger(__name__)
5151
prefix = "config/"
52+
uuid_regex = (
53+
r"[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"
54+
)
5255
Config = load_model("config", "Config")
5356
Device = load_model("config", "Device")
5457
DeviceGroup = load_model("config", "DeviceGroup")
@@ -166,9 +169,19 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
166169
def get_urls(self):
167170
options = getattr(self.model, "_meta")
168171
url_prefix = "{0}_{1}".format(options.app_label, options.model_name)
169-
return [
172+
default_urls = super().get_urls()
173+
safe_urls = []
174+
for url in default_urls:
175+
if url.name and not (
176+
url.name.endswith("_change")
177+
or url.name.endswith("_history")
178+
or url.name.endswith("_delete")
179+
):
180+
safe_urls.append(url)
181+
strict_urls = [
182+
# custom app URLs
170183
re_path(
171-
r"^download/(?P<pk>[^/]+)/$",
184+
rf"^download/(?P<pk>{uuid_regex})/$",
172185
self.admin_site.admin_view(self.download_view),
173186
name="{0}_download".format(url_prefix),
174187
),
@@ -178,11 +191,28 @@ def get_urls(self):
178191
name="{0}_preview".format(url_prefix),
179192
),
180193
re_path(
181-
r"^(?P<pk>[^/]+)/context\.json$",
194+
rf"^(?P<pk>{uuid_regex})/context\.json$",
182195
self.admin_site.admin_view(self.context_view),
183196
name="{0}_context".format(url_prefix),
184197
),
185-
] + super().get_urls()
198+
# strict overrides for the default admin views
199+
re_path(
200+
rf"^(?P<object_id>({uuid_regex}|__fk__))/history/$",
201+
self.admin_site.admin_view(self.history_view),
202+
name=f"{url_prefix}_history",
203+
),
204+
re_path(
205+
rf"^(?P<object_id>({uuid_regex}|__fk__))/delete/$",
206+
self.admin_site.admin_view(self.delete_view),
207+
name=f"{url_prefix}_delete",
208+
),
209+
re_path(
210+
rf"^(?P<object_id>({uuid_regex}|__fk__))/change/$",
211+
self.admin_site.admin_view(self.change_view),
212+
name=f"{url_prefix}_change",
213+
),
214+
]
215+
return strict_urls + safe_urls
186216

187217
def _get_config_model(self):
188218
model = self.model

openwisp_controller/config/tests/pytest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
Device = load_model("config", "Device")
1515

16+
uuid_regex = (
17+
r"[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"
18+
)
19+
1620

1721
@pytest.mark.asyncio
1822
@pytest.mark.django_db(transaction=True)
@@ -25,7 +29,7 @@ class TestDeviceConsumer(CreateDeviceMixin):
2529
URLRouter(
2630
[
2731
re_path(
28-
r"^ws/controller/device/(?P<pk>[^/]+)/$",
32+
rf"^ws/controller/device/(?P<pk>{uuid_regex})/$",
2933
BaseDeviceConsumer.as_asgi(),
3034
)
3135
]

openwisp_controller/config/tests/test_admin.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,52 @@ def test_change_device_malformed_uuid(self):
14051405
response = self.client.get(path)
14061406
self.assertEqual(response.status_code, 404)
14071407

1408+
def test_malformed_admin_urls_return_404(self):
1409+
device = self._create_device()
1410+
template = self._create_template()
1411+
vpn = self._create_vpn()
1412+
test_cases = [
1413+
("device", device.pk, "config_device"),
1414+
("template", template.pk, "config_template"),
1415+
("vpn", vpn.pk, "config_vpn"),
1416+
]
1417+
junk_path = "some/junk/path/here/"
1418+
original_bug_junk = "history/1564/undefinedadmin/img/icon-deletelink.svg"
1419+
1420+
for model_name, valid_pk, model_admin_name in test_cases:
1421+
with self.subTest(model=model_name):
1422+
change_url_name = f"admin:{model_admin_name}_change"
1423+
valid_change_url = reverse(change_url_name, args=[valid_pk])
1424+
malformed_change_url = f"{valid_change_url}{junk_path}"
1425+
response_change = self.client.get(malformed_change_url, follow=False)
1426+
1427+
self.assertEqual(
1428+
response_change.status_code,
1429+
404,
1430+
(f'Malformed "change" URL for {model_name} did not return 404. '),
1431+
)
1432+
history_url_name = f"admin:{model_admin_name}_history"
1433+
valid_history_url = reverse(history_url_name, args=[valid_pk])
1434+
malformed_history_url = f"{valid_history_url}{junk_path}"
1435+
response_history = self.client.get(malformed_history_url, follow=False)
1436+
1437+
self.assertEqual(
1438+
response_history.status_code,
1439+
404,
1440+
(f'Malformed "history" URL for {model_name} did not return 404. '),
1441+
)
1442+
original_bug_url = f"{valid_history_url}{original_bug_junk}"
1443+
response_original_bug = self.client.get(original_bug_url, follow=False)
1444+
1445+
self.assertEqual(
1446+
response_original_bug.status_code,
1447+
404,
1448+
(
1449+
f"Original bug URL for {model_name} did not return 404. "
1450+
"This may be a regression of bug #682."
1451+
),
1452+
)
1453+
14081454
def test_uuid_field_in_change(self):
14091455
t = Template.objects.first()
14101456
d = self._create_device()

openwisp_controller/config/tests/test_controller.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,8 @@ def test_device_checksum_requested_signal_is_emitted(self):
257257
def test_device_checksum_bad_uuid(self):
258258
d = self._create_device_config()
259259
pk = "{}-wrong".format(d.pk)
260-
response = self.client.get(
261-
reverse("controller:device_checksum", args=[pk]), {"key": d.key}
262-
)
260+
bad_path = f"/controller/device/checksum/{pk}/"
261+
response = self.client.get(bad_path, {"key": d.key})
263262
self.assertEqual(response.status_code, 404)
264263

265264
def test_device_config_download_requested_signal_is_emitted(self):
@@ -323,9 +322,8 @@ def test_deactivated_device_download_config(self):
323322
def test_device_download_config_bad_uuid(self):
324323
d = self._create_device_config()
325324
pk = "{}-wrong".format(d.pk)
326-
response = self.client.get(
327-
reverse("controller:device_download_config", args=[pk]), {"key": d.key}
328-
)
325+
bad_path = f"/controller/device/download-config/{pk}/"
326+
response = self.client.get(bad_path, {"key": d.key})
329327
self.assertEqual(response.status_code, 404)
330328

331329
def test_vpn_checksum_requested_signal_is_emitted(self):
@@ -383,9 +381,8 @@ def test_vpn_checksum(self):
383381
def test_vpn_checksum_bad_uuid(self):
384382
v = self._create_vpn()
385383
pk = "{}-wrong".format(v.pk)
386-
response = self.client.get(
387-
reverse("controller:vpn_checksum", args=[pk]), {"key": v.key}
388-
)
384+
bad_path = f"/controller/vpn/checksum/{pk}/"
385+
response = self.client.get(bad_path, {"key": v.key})
389386
self.assertEqual(response.status_code, 404)
390387

391388
@capture_any_output()
@@ -500,9 +497,8 @@ def test_vpn_download_config(self):
500497
def test_vpn_download_config_bad_uuid(self):
501498
v = self._create_vpn()
502499
pk = "{}-wrong".format(v.pk)
503-
response = self.client.get(
504-
reverse("controller:vpn_download_config", args=[pk]), {"key": v.key}
505-
)
500+
bad_path = f"/controller/vpn/download-config/{pk}/"
501+
response = self.client.get(bad_path, {"key": v.key})
506502
self.assertEqual(response.status_code, 404)
507503

508504
@capture_any_output()
@@ -895,9 +891,8 @@ def test_deactivated_device_report_status(self):
895891
def test_device_report_status_bad_uuid(self):
896892
d = self._create_device_config()
897893
pk = "{}-wrong".format(d.pk)
898-
response = self.client.post(
899-
reverse("controller:device_report_status", args=[pk]), {"key": d.key}
900-
)
894+
bad_path = f"/controller/device/report-status/{pk}/"
895+
response = self.client.post(bad_path, {"key": d.key})
901896
self.assertEqual(response.status_code, 404)
902897

903898
@capture_any_output()
@@ -1012,9 +1007,8 @@ def test_device_update_info_bad_uuid(self):
10121007
"os": "OpenWrt 18.06-SNAPSHOT r7312-e60be11330",
10131008
"system": "Atheros AR9344 rev 3",
10141009
}
1015-
response = self.client.post(
1016-
reverse("controller:device_update_info", args=[pk]), params
1017-
)
1010+
bad_path = f"/controller/device/update-info/{pk}/"
1011+
response = self.client.post(bad_path, params)
10181012
self.assertEqual(response.status_code, 404)
10191013

10201014
def test_device_update_info_400(self):

openwisp_controller/config/utils.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from openwisp_notifications.utils import _get_object_link
99

1010
logger = logging.getLogger(__name__)
11+
uuid_regex = (
12+
r"[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"
13+
)
1114

1215

1316
def get_object_or_404(model, **kwargs):
@@ -117,22 +120,22 @@ def get_controller_urls(views_module):
117120
"""
118121
urls = [
119122
re_path(
120-
"controller/device/checksum/(?P<pk>[^/]+)/$",
123+
rf"controller/device/checksum/(?P<pk>{uuid_regex})/$",
121124
views_module.device_checksum,
122125
name="device_checksum",
123126
),
124127
re_path(
125-
"controller/device/download-config/(?P<pk>[^/]+)/$",
128+
rf"controller/device/download-config/(?P<pk>{uuid_regex})/$",
126129
views_module.device_download_config,
127130
name="device_download_config",
128131
),
129132
re_path(
130-
"controller/device/update-info/(?P<pk>[^/]+)/$",
133+
rf"controller/device/update-info/(?P<pk>{uuid_regex})/$",
131134
views_module.device_update_info,
132135
name="device_update_info",
133136
),
134137
re_path(
135-
"controller/device/report-status/(?P<pk>[^/]+)/$",
138+
rf"controller/device/report-status/(?P<pk>{uuid_regex})/$",
136139
views_module.device_report_status,
137140
name="device_report_status",
138141
),
@@ -142,33 +145,33 @@ def get_controller_urls(views_module):
142145
name="device_register",
143146
),
144147
re_path(
145-
"controller/vpn/checksum/(?P<pk>[^/]+)/$",
148+
rf"controller/vpn/checksum/(?P<pk>{uuid_regex})/$",
146149
views_module.vpn_checksum,
147150
name="vpn_checksum",
148151
),
149152
re_path(
150-
"controller/vpn/download-config/(?P<pk>[^/]+)/$",
153+
rf"controller/vpn/download-config/(?P<pk>{uuid_regex})/$",
151154
views_module.vpn_download_config,
152155
name="vpn_download_config",
153156
),
154157
# legacy URLs
155158
re_path(
156-
"controller/checksum/(?P<pk>[^/]+)/$",
159+
rf"controller/checksum/(?P<pk>{uuid_regex})/$",
157160
views_module.device_checksum,
158161
name="checksum_legacy",
159162
),
160163
re_path(
161-
"controller/download-config/(?P<pk>[^/]+)/$",
164+
rf"controller/download-config/(?P<pk>{uuid_regex})/$",
162165
views_module.device_download_config,
163166
name="download_config_legacy",
164167
),
165168
re_path(
166-
"controller/update-info/(?P<pk>[^/]+)/$",
169+
rf"controller/update-info/(?P<pk>{uuid_regex})/$",
167170
views_module.device_update_info,
168171
name="update_info_legacy",
169172
),
170173
re_path(
171-
"controller/report-status/(?P<pk>[^/]+)/$",
174+
rf"controller/report-status/(?P<pk>{uuid_regex})/$",
172175
views_module.device_report_status,
173176
name="report_status_legacy",
174177
),

openwisp_controller/connection/channels/routing.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
from . import consumers as ow_consumer
44

5+
uuid_regex = (
6+
r"[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"
7+
)
8+
59

610
def get_routes(consumer=ow_consumer):
711
return [
812
re_path(
9-
r"^ws/controller/device/(?P<pk>[^/]+)/command$",
13+
rf"^ws/controller/device/(?P<pk>{uuid_regex})/command$",
1014
consumer.CommandConsumer.as_asgi(),
1115
)
1216
]

0 commit comments

Comments
 (0)