Skip to content

Commit 4ccd46d

Browse files
committed
Obj existence prior to admin-permission moved past
- There comment that there was a leak on CloneTo and CloneFrom is not applicable, the token is already set to a volume, it is just checking if things are alright, volume pool still exists etc; - The existence leak is sometimes not possible to solved in the admin/api.py, because it would limit the sanitization made before the fire_event_for_permission(); - When the leak was avoided without compromising sanitization, it makes the logic a bit strange, some validation being done past fire_event_for_permission().
1 parent 7e694d7 commit 4ccd46d

2 files changed

Lines changed: 62 additions & 38 deletions

File tree

qubes/api/admin.py

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -454,15 +454,22 @@ async def vm_volume_revert(self, untrusted_payload):
454454
)
455455
untrusted_revision = untrusted_payload.decode("ascii").strip()
456456
del untrusted_payload
457+
allowed_chars = string.ascii_letters + string.digits + "-_."
458+
self.enforce(
459+
all(c in allowed_chars for c in untrusted_revision),
460+
reason="Revision name must be in safe set: " + allowed_chars,
461+
)
462+
revision = untrusted_revision
463+
del untrusted_revision
457464

458465
volume = self.dest.volumes[self.arg]
466+
467+
self.fire_event_for_permission(volume=volume, revision=revision)
468+
459469
snapshots = volume.revisions
460-
# TODO: ben: info-leak: revision existence
461-
if untrusted_revision not in snapshots:
470+
if revision not in snapshots:
462471
raise qubes.exc.QubesVolumeRevisionNotFoundError()
463-
revision = untrusted_revision
464472

465-
self.fire_event_for_permission(volume=volume, revision=revision)
466473
await qubes.utils.coro_maybe(volume.revert(revision))
467474
self.app.save()
468475

@@ -519,10 +526,8 @@ async def vm_volume_clone_to(self, untrusted_payload):
519526
del self.app.api_admin_pending_clone[token]
520527

521528
# make sure the volume still exists, but invalidate token anyway
522-
# TODO: ben: info-leak: pool existence
523529
if str(src_volume.pool) not in self.app.pools:
524530
raise qubes.exc.QubesPoolNotFoundError()
525-
# TODO: ben: info-leak: volume existence
526531
if src_volume not in self.app.pools[str(src_volume.pool)].volumes:
527532
raise qubes.exc.QubesVolumeNotFoundError()
528533

@@ -796,9 +801,9 @@ async def pool_usage(self):
796801
@qubes.api.method("admin.pool.Add", scope="global", write=True)
797802
async def pool_add(self, untrusted_payload):
798803
self.enforce_dest_dom0(wants=True)
799-
# TODO: ben: info-leak: pool existence
804+
# TODO: ben: info-leak: driver existence: can be done on the policy
800805
self.enforce_arg(
801-
wants=qubes.storage.pool_drivers(), short_reason="available pools"
806+
wants=qubes.storage.pool_drivers(), short_reason="available drivers"
802807
)
803808

804809
try:
@@ -838,9 +843,9 @@ async def pool_add(self, untrusted_payload):
838843
reason="Pool name must be in safe set: " + allowed_chars,
839844
)
840845
pool_name = untrusted_pool_name
841-
# TODO: ben: info-leak: pool existence
846+
invalid_pool = False
842847
if pool_name in self.app.pools:
843-
raise qubes.exc.QubesPoolInUseError(pool_name=pool_name)
848+
invalid_pool = True
844849

845850
driver_parameters = qubes.storage.driver_parameters(self.arg)
846851
dp_names = driver_parameters.keys()
@@ -864,6 +869,9 @@ async def pool_add(self, untrusted_payload):
864869

865870
self.fire_event_for_permission(name=pool_name, pool_config=pool_config)
866871

872+
if invalid_pool:
873+
raise qubes.exc.QubesPoolInUseError(pool_name=pool_name)
874+
867875
await self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
868876
self.app.save()
869877

@@ -955,7 +963,7 @@ async def label_get(self):
955963
self.enforce_dest_dom0(wants=True)
956964
qubes.utils.validate_label(untrusted_label=self.arg)
957965

958-
# TODO: ben: info-leak: label existence
966+
# TODO: ben: info-leak: label existence: can be solved via the policy
959967
label = self.app.get_label(self.arg)
960968

961969
self.fire_event_for_permission(label=label)
@@ -969,7 +977,7 @@ async def label_index(self):
969977
self.enforce_dest_dom0(wants=True)
970978
qubes.utils.validate_label(untrusted_label=self.arg)
971979

972-
# TODO: ben: info-leak: label existence
980+
# TODO: ben: info-leak: label existence: can be solved via the policy
973981
label = self.app.get_label(self.arg)
974982

975983
self.fire_event_for_permission(label=label)
@@ -981,15 +989,6 @@ async def label_create(self, untrusted_payload):
981989
self.enforce_dest_dom0(wants=True)
982990
qubes.utils.validate_label(untrusted_label=self.arg)
983991

984-
# TODO: ben: info-leak: label existence
985-
try:
986-
self.app.get_label(self.arg)
987-
except KeyError:
988-
# ok, no such label yet
989-
pass
990-
else:
991-
raise qubes.exc.QubesLabelInUseError(label=self.arg)
992-
993992
qubes.utils.validate_label_value(
994993
untrusted_label_value=untrusted_payload
995994
)
@@ -1000,6 +999,14 @@ async def label_create(self, untrusted_payload):
1000999

10011000
self.fire_event_for_permission(color=color)
10021001

1002+
try:
1003+
self.app.get_label(self.arg)
1004+
except KeyError:
1005+
# ok, no such label yet
1006+
pass
1007+
else:
1008+
raise qubes.exc.QubesLabelInUseError(label=self.arg)
1009+
10031010
# allocate new index, but make sure it's outside of default labels set
10041011
new_index = (
10051012
max(qubes.config.max_default_label, *self.app.labels.keys()) + 1
@@ -1016,15 +1023,16 @@ async def label_remove(self):
10161023
self.enforce_dest_dom0(wants=True)
10171024
qubes.utils.validate_label(untrusted_label=self.arg)
10181025

1019-
# TODO: ben: info-leak: label existence
1026+
# TODO: ben: info-leak: label existence: can be solved via the policy
10201027
label = self.app.get_label(self.arg)
1028+
1029+
self.fire_event_for_permission(label=label)
1030+
10211031
self.enforce(
10221032
label.index > qubes.config.max_default_label,
10231033
reason="Can only remove custom labels",
10241034
)
10251035

1026-
self.fire_event_for_permission(label=label)
1027-
10281036
# FIXME: this should be in app.add_label()
10291037
for vm in self.app.domains:
10301038
if vm.label == label:
@@ -1315,17 +1323,18 @@ async def _vm_create(
13151323

13161324
# if argument is given, it needs to be a valid template, and only
13171325
# when given VM class do need a template
1326+
invalid_template = False
13181327
if self.arg:
1319-
if hasattr(vm_class, "template"):
1320-
# TODO: ben: info-leak: template existence
1321-
if self.arg not in self.app.domains:
1322-
raise qubes.exc.QubesVMNotFoundError(self.arg)
1323-
kwargs["template"] = self.app.domains[self.arg]
1324-
else:
1328+
if not hasattr(vm_class, "template"):
13251329
raise qubes.exc.ProtocolError(
13261330
"{} cannot be based on template".format(vm_type)
13271331
)
1332+
if self.arg in self.app.domains:
1333+
kwargs["template"] = self.app.domains[self.arg]
1334+
else:
1335+
invalid_template = True
13281336

1337+
invalid_label = False
13291338
for untrusted_param in untrusted_payload.decode(
13301339
"ascii", errors="strict"
13311340
).split(" "):
@@ -1349,9 +1358,11 @@ async def _vm_create(
13491358
all(c in allowed_chars for c in untrusted_value),
13501359
reason="Label must be in safe set: " + allowed_chars,
13511360
)
1352-
# TODO: ben: info-leak: label existence
13531361
qubes.utils.validate_label(untrusted_label=untrusted_value)
1354-
kwargs["label"] = self.app.get_label(untrusted_value)
1362+
try:
1363+
kwargs["label"] = self.app.get_label(untrusted_value)
1364+
except qubes.exc.QubesLabelNotFoundError:
1365+
invalid_label = True
13551366

13561367
elif untrusted_key == "pool" and allow_pool:
13571368
self.enforce(
@@ -1400,13 +1411,27 @@ async def _vm_create(
14001411
reason="Conflicting options specified: 'pool=', 'pool:volume='",
14011412
)
14021413

1403-
# TODO: ben: info-leak: qube existence
1414+
invalid_name = False
14041415
if kwargs["name"] in self.app.domains:
1416+
invalid_name = True
1417+
1418+
self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
1419+
1420+
# Avoids leaking qube existence before admin-permission.
1421+
if invalid_name:
14051422
raise qubes.exc.ProtocolError(
14061423
"Qube already exists: {}".format(kwargs["name"])
14071424
)
14081425

1409-
self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
1426+
# Avoids leaking template existence before admin-permission.
1427+
if invalid_template:
1428+
raise qubes.exc.QubesVMNotFoundError(self.arg)
1429+
1430+
# Avoids leaking label existence before admin-permission.
1431+
if invalid_label:
1432+
# If label is invalid, it came from payload and should not be
1433+
# logged.
1434+
raise qubes.exc.QubesLabelNotFoundError(label="untrusted label")
14101435

14111436
vm = self.app.add_new_vm(vm_class, **kwargs)
14121437
# TODO: move this to extension (in race-free fashion)

qubes/tests/api_admin.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,9 +1315,6 @@ def test_200_label_create_invalid_color(self):
13151315
self.call_mgmt_func(
13161316
b"admin.label.Create", b"dom0", b"cyan", b"abcd"
13171317
)
1318-
self.assertEqual(
1319-
self.app.get_label.mock_calls, [unittest.mock.call("cyan")]
1320-
)
13211318
self.assertEqual(self.app.labels.mock_calls, [])
13221319
self.assertFalse(self.app.save.called)
13231320

@@ -1352,7 +1349,9 @@ def test_200_label_create_invalid_name(self):
13521349
def test_200_label_create_already_exists(self):
13531350
self.app.get_label = unittest.mock.Mock(wraps=self.app.get_label)
13541351
with self.assertRaises(qubes.exc.QubesLabelInUseError):
1355-
self.call_mgmt_func(b"admin.label.Create", b"dom0", b"red", b"abcd")
1352+
self.call_mgmt_func(
1353+
b"admin.label.Create", b"dom0", b"red", b"0xff0000"
1354+
)
13561355
self.assertEqual(
13571356
self.app.get_label.mock_calls, [unittest.mock.call("red")]
13581357
)

0 commit comments

Comments
 (0)