From 61ef4548a1c7b33b066a27db0fb661b54bb83c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Sat, 21 Mar 2026 10:05:28 -0600 Subject: [PATCH 1/2] fix: enforce strict RFC 4122 UUID validation in *_from_uid methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Malformed UUIDs (e.g. "aaa-bbb", "not-a-uuid") were silently passed to the OpenStack API, producing cryptic 404 or 400 errors. Now server_from_uid(), image_from_uid(), and all spec-generated getfromid methods validate input against the 8-4-4-4-12 hex format before making any API call, with a clear die message. The _looks_valid_id() heuristic is intentionally left loose — it serves as an ID-vs-name routing check, and OpenStack resources like flavors use simple numeric IDs. Strict UUID validation is applied at the entry points where UUIDs are the expected format. Test fixture UUIDs that used non-compliant formats have been corrected to proper 8-4-4-4-12 format. Co-Authored-By: Claude Opus 4.6 --- lib/OpenStack/MetaAPI/API/Images.pm | 2 ++ lib/OpenStack/MetaAPI/Roles/GetFromId.pm | 5 +++++ t/service-compute.t | 24 ++++++++++++++++++++++++ t/service-image.t | 24 ++++++++++++++++++++++-- t/xtra-create-vm.t | 20 ++++++++++---------- t/xtra-delete-vm.t | 20 ++++++++++---------- xt/create_vm.t | 2 +- 7 files changed, 74 insertions(+), 23 deletions(-) diff --git a/lib/OpenStack/MetaAPI/API/Images.pm b/lib/OpenStack/MetaAPI/API/Images.pm index 8c9ad31..693d336 100644 --- a/lib/OpenStack/MetaAPI/API/Images.pm +++ b/lib/OpenStack/MetaAPI/API/Images.pm @@ -59,6 +59,8 @@ sub image_from_uid { my ($self, $uid) = @_; die "image_from_uid: uid is required" unless defined $uid; + die "Invalid UUID format '$uid' - expected 8-4-4-4-12 hex format" + unless $uid =~ m{^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$}i; my $uri = $self->root_uri('/images/' . $uid); diff --git a/lib/OpenStack/MetaAPI/Roles/GetFromId.pm b/lib/OpenStack/MetaAPI/Roles/GetFromId.pm index 9a1a1ca..905afd2 100644 --- a/lib/OpenStack/MetaAPI/Roles/GetFromId.pm +++ b/lib/OpenStack/MetaAPI/Roles/GetFromId.pm @@ -5,12 +5,17 @@ use warnings; use Moo::Role; +# RFC 4122 UUID: 8-4-4-4-12 hex groups +my $VALID_UUID = qr{^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$}i; + sub _get_from_id_spec { my ($self, $route, $id) = @_; die "route must be defined when using get_from_id" unless defined $route; die "invalid route '$route' - must starts with /" unless $route =~ m{^/}; die "Undefined 'id' for route '$route'" unless defined $id; + die "Invalid UUID format '$id' for route '$route' - expected 8-4-4-4-12 hex format" + unless $id =~ $VALID_UUID; $route .= '/' unless $route =~ m{/$}; diff --git a/t/service-compute.t b/t/service-compute.t index 105f8cd..87a0bfd 100644 --- a/t/service-compute.t +++ b/t/service-compute.t @@ -108,6 +108,30 @@ ok $api, "got one api object" or die; } +{ + note "Testing server_from_uid rejects malformed UUIDs"; + + like dies { $api->server_from_uid('not-a-uuid') }, + qr/Invalid UUID format/, + "server_from_uid rejects non-UUID string"; + + like dies { $api->server_from_uid('aaa-bbb-ccc') }, + qr/Invalid UUID format/, + "server_from_uid rejects too-short hex-dash string"; + + like dies { $api->server_from_uid('33748c2338dd4f70b774522fc69e7b67') }, + qr/Invalid UUID format/, + "server_from_uid rejects UUID without dashes"; + + like dies { $api->server_from_uid('33748c23-38dd-4f70-b774') }, + qr/Invalid UUID format/, + "server_from_uid rejects truncated UUID"; + + like dies { $api->server_from_uid('ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ') }, + qr/Invalid UUID format/, + "server_from_uid rejects non-hex characters"; +} + done_testing; sub json_for_server { diff --git a/t/service-image.t b/t/service-image.t index 20ef66a..8ce8970 100644 --- a/t/service-image.t +++ b/t/service-image.t @@ -46,7 +46,7 @@ ok $api, "got one api object" or die; 'created_at' => '2019-04-10T20:23:09Z', 'disk_format' => 'raw', 'file' => '/v2/images/6056cbf415fd5f8c223c8a69341e44ee/file', - 'id' => 'b763530-fe64d-2116b5-c627a7-7f0cf71b', + 'id' => 'b7635300-fe6d-2116-c627-7f0cf71b0000', 'min_disk' => 0, 'min_ram' => 0, 'name' => 'MyImage', @@ -104,6 +104,26 @@ ok $api, "got one api object" or die; } +{ + note "Testing image_from_uid rejects malformed UUIDs"; + + like dies { $api->image_from_uid('not-a-uuid') }, + qr/Invalid UUID format/, + "image_from_uid rejects non-UUID string"; + + like dies { $api->image_from_uid('aaa-bbb-ccc') }, + qr/Invalid UUID format/, + "image_from_uid rejects too-short hex-dash string"; + + like dies { $api->image_from_uid('170fafa513294a3c9c279bb77b77206d') }, + qr/Invalid UUID format/, + "image_from_uid rejects UUID without dashes"; + + like dies { $api->image_from_uid('170fafa5-1329-44a3-9c27') }, + qr/Invalid UUID format/, + "image_from_uid rejects truncated UUID"; +} + done_testing; sub json_for_image { @@ -112,7 +132,7 @@ sub json_for_image { return <<'JSON'; { "min_ram" : 0, - "id" : "b763530-fe64d-2116b5-c627a7-7f0cf71b", + "id" : "b7635300-fe6d-2116-c627-7f0cf71b0000", "os_version" : "7", "created_at" : "2019-04-10T20:23:09Z", "os_arch" : "x86_64", diff --git a/t/xtra-create-vm.t b/t/xtra-create-vm.t index 721dc0e..25991d3 100644 --- a/t/xtra-create-vm.t +++ b/t/xtra-create-vm.t @@ -80,7 +80,7 @@ ok $api, "got one api object" or die; ); mock_get_request( - 'http://127.0.0.1:8774/v2.1/servers/aaaaa-bbbb-ccccc-dddd', + 'http://127.0.0.1:8774/v2.1/servers/aaaaabbb-bccc-cddd-d000-000000000000', application_json(json_for_server()), ); @@ -99,7 +99,7 @@ ok $api, "got one api object" or die; my $iteration = 0; mock_get_request( - 'http://127.0.0.1:8774/v2.1/servers/aaaaa-bbbb-ccccc-dddd', + 'http://127.0.0.1:8774/v2.1/servers/aaaaabbb-bccc-cddd-d000-000000000000', sub { my ($request) = @_; note "checking server state ", ++$iteration; @@ -115,7 +115,7 @@ ok $api, "got one api object" or die; # then the server is active - die "Too many calls to servers/aaaaa-bbbb-ccccc-dddd" + die "Too many calls to servers/aaaaabbb-bccc-cddd-d000-000000000000" if $iteration >= 10; return { @@ -139,12 +139,12 @@ ok $api, "got one api object" or die; ); mock_get_request( - 'http://127.0.0.1:9696/v2.0/ports?device_id=aaaaa-bbbb-ccccc-dddd', + 'http://127.0.0.1:9696/v2.0/ports?device_id=aaaaabbb-bccc-cddd-d000-000000000000', application_json(json_for_ports_device_id()), ); mock_put_request( - 'http://127.0.0.1:9696/v2.0/floatingips/ffffff-000000-fffff-111111-7777777', + 'http://127.0.0.1:9696/v2.0/floatingips/ffffff00-0000-ffff-1111-117777777777', application_json(json_for_put_floatingips()), ); @@ -153,7 +153,7 @@ ok $api, "got one api object" or die; is $vm => hash { field id => '33748c23-38dd-4f70-b774-522fc69e7b67'; field floating_ip_address => '10.1.2.3'; - field floating_ip_id => 'ffffff-000000-fffff-111111-7777777'; + field floating_ip_id => 'ffffff00-0000-ffff-1111-117777777777'; field status => 'ACTIVE'; field user_id => 'fake'; @@ -233,7 +233,7 @@ sub json_for_ports_device_id { "created_at": "2016-03-08T20:19:41", "data_plane_status": null, "description": "", - "device_id": "aaaaa-bbbb-ccccc-dddd", + "device_id": "aaaaabbb-bccc-cddd-d000-000000000000", "device_owner": "network:router_gateway", "dns_assignment": { "hostname": "myport", @@ -284,7 +284,7 @@ sub json_for_put_floatingips { "fixed_ip_address": "172.24.4.228", "floating_ip_address": "10.1.2.3", "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", - "id": "ffffff-000000-fffff-111111-7777777", + "id": "ffffff00-0000-ffff-1111-117777777777", "description": "floating ip for testing", "dns_domain": "my-domain.org.", "dns_name": "myfip", @@ -318,7 +318,7 @@ sub json_for_post_floatingips { "fixed_ip_address": "172.24.4.228", "floating_ip_address": "10.1.2.3", "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", - "id": "ffffff-000000-fffff-111111-7777777", + "id": "ffffff00-0000-ffff-1111-117777777777", "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab", "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", "status": "ACTIVE", @@ -444,7 +444,7 @@ sub json_create_server { return <<'JSON'; { "server" : { - "id": "aaaaa-bbbb-ccccc-dddd", + "id": "aaaaabbb-bccc-cddd-d000-000000000000", "accessIPv4": "1.2.3.4", "accessIPv6": "80fe::", "name" : "new-server-test", diff --git a/t/xtra-delete-vm.t b/t/xtra-delete-vm.t index a010435..82a9476 100644 --- a/t/xtra-delete-vm.t +++ b/t/xtra-delete-vm.t @@ -36,17 +36,17 @@ my $SERVER_NAME = 'testsuite OpenStack::MetaAPI'; ); mock_get_request( - 'http://127.0.0.1:8774/v2.1/servers/000000-1111-22222-33333-444444', + 'http://127.0.0.1:8774/v2.1/servers/00000000-1111-2222-3333-444444444444', application_json(json_servers_id()), ); mock_get_request( - 'http://127.0.0.1:9696/v2.0/ports?device_id=000000-1111-22222-33333-444444', + 'http://127.0.0.1:9696/v2.0/ports?device_id=00000000-1111-2222-3333-444444444444', application_json(json_for_ports_device_id_unused()), ); mock_delete_request( - 'http://127.0.0.1:8774/v2.1/servers/000000-1111-22222-33333-444444', + 'http://127.0.0.1:8774/v2.1/servers/00000000-1111-2222-3333-444444444444', txt_plain("ok delete server"), ); @@ -57,7 +57,7 @@ my $SERVER_NAME = 'testsuite OpenStack::MetaAPI'; } mock_get_request( - 'http://127.0.0.1:9696/v2.0/ports?device_id=000000-1111-22222-33333-444444', + 'http://127.0.0.1:9696/v2.0/ports?device_id=00000000-1111-2222-3333-444444444444', application_json(json_for_ports_device_id_used()), ); @@ -67,7 +67,7 @@ my $SERVER_NAME = 'testsuite OpenStack::MetaAPI'; ); mock_delete_request( - 'http://127.0.0.1:9696/v2.0/floatingips/ffff-1111-00000-aaaaaaa-777777', + 'http://127.0.0.1:9696/v2.0/floatingips/ffff1111-0000-0aaa-aaaa-777777777777', txt_plain("ok delete floating ip ok"), ); @@ -148,7 +148,7 @@ sub json_for_floatingips { "fixed_ip_address": "10.0.0.3", "floating_ip_address": "172.24.4.228", "port_id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", - "id": "ffff-1111-00000-aaaaaaa-777777", + "id": "ffff1111-0000-0aaa-aaaa-777777777777", "status": "ACTIVE", "port_details": { "status": "ACTIVE", @@ -177,7 +177,7 @@ sub json_for_ports_device_id_used { "created_at": "2016-03-08T20:19:41", "data_plane_status": null, "description": "", - "device_id": "000000-1111-22222-33333-444444", + "device_id": "00000000-1111-2222-3333-444444444444", "device_owner": "network:router_gateway", "dns_assignment": { "hostname": "myport", @@ -230,7 +230,7 @@ sub json_for_ports_device_id_unused { "created_at": "2016-03-08T20:19:41", "data_plane_status": null, "description": "", - "device_id": "0000-00000-0000-0000-0000", + "device_id": "00000000-0000-0000-0000-000000000000", "device_owner": "network:router_gateway", "dns_assignment": { "hostname": "myport", @@ -359,7 +359,7 @@ sub json_servers_id { "swap": 0, "vcpus": 1 }, - "id": "000000-1111-22222-33333-444444", + "id": "00000000-1111-2222-3333-444444444444", "image": { "id": "70a599e0-31e7-49b7-b260-868f441e862b", "links": [ @@ -393,7 +393,7 @@ sub json_servers { { "servers": [ { - "id": "000000-1111-22222-33333-444444", + "id": "00000000-1111-2222-3333-444444444444", "links": [ { "href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/servers/22c91117-08de-4894-9aa9-6ef382400985", diff --git a/xt/create_vm.t b/xt/create_vm.t index aedabbc..2d726ac 100644 --- a/xt/create_vm.t +++ b/xt/create_vm.t @@ -16,7 +16,7 @@ use lib "$FindBin::Bin/../t/lib"; use Test::OpenStack::MetaAPI qw{:all}; -my $VALID_ID = match qr{^[a-f0-9\-]+$}; +my $VALID_ID = match qr{^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$}i; my $IMAGE_UID = '170fafa5-1329-44a3-9c27-9bb77b77206d'; my $IMAGE_NAME = 'myimage'; From 8eef092572e4b5cdcd07267fcc86dcaed2fea151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Thu, 10 Sep 2026 18:38:50 -0600 Subject: [PATCH 2/2] fix: correct non-RFC-4122 server UID fixtures in create_vm and roles tests --- t/create-vm-error-status.t | 4 ++-- t/create-vm-no-floating-ip.t | 4 ++-- t/roles.t | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/t/create-vm-error-status.t b/t/create-vm-error-status.t index 0ba6816..24d9323 100644 --- a/t/create-vm-error-status.t +++ b/t/create-vm-error-status.t @@ -25,7 +25,7 @@ my $api = get_api_object(use_env => 0); ok $api, "got one api object" or die; my $IMAGE_UID = '170fafa5-1329-44a3-9c27-9bb77b77206d'; -my $SERVER_UID = 'aaaaa-bbbb-ccccc-dddd'; +my $SERVER_UID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; mock_get_request( 'http://127.0.0.1:8774/v2.1/flavors', @@ -144,7 +144,7 @@ sub json_create_server { return <<'JSON'; { "server" : { - "id": "aaaaa-bbbb-ccccc-dddd", + "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "name" : "vm-error-test" } } diff --git a/t/create-vm-no-floating-ip.t b/t/create-vm-no-floating-ip.t index 2820cda..382b110 100644 --- a/t/create-vm-no-floating-ip.t +++ b/t/create-vm-no-floating-ip.t @@ -28,7 +28,7 @@ my $api = get_api_object(use_env => 0); ok $api, "got one api object" or die; my $IMAGE_UID = '170fafa5-1329-44a3-9c27-9bb77b77206d'; -my $SERVER_UID = 'aaaaa-bbbb-ccccc-dddd'; +my $SERVER_UID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; mock_get_request( 'http://127.0.0.1:8774/v2.1/flavors', @@ -101,7 +101,7 @@ sub json_create_server { return <<'JSON'; { "server" : { - "id": "aaaaa-bbbb-ccccc-dddd", + "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "name" : "vm-no-floating-ip" } } diff --git a/t/roles.t b/t/roles.t index d911832..92af631 100644 --- a/t/roles.t +++ b/t/roles.t @@ -162,10 +162,10 @@ mock_lwp_useragent(); my $api = get_api_object(use_env => 0); ok $api, "got api object for getfromid test" or die; - my $server_uid = 'abc-def-123'; + my $server_uid = 'abcdef01-2345-6789-abcd-ef0123456789'; mock_get_request( "http://127.0.0.1:8774/v2.1/servers/$server_uid", - application_json('{"server": {"id": "abc-def-123", "name": "my-server", "status": "ACTIVE"}}'), + application_json('{"server": {"id": "abcdef01-2345-6789-abcd-ef0123456789", "name": "my-server", "status": "ACTIVE"}}'), ); my $server = $api->server_from_uid($server_uid);