Skip to content

[BUG] string.uuid and string.tuuid accept values with an undefined UUID version or variant #530

Closed

Description

@otnc

Description

string.uuid is documented as requiring "a valid UUID as defined by RFC 4122", but the rule only checks the 8-4-4-4-12 hex shape:

this.matches('^[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}$')

RFC 4122, and RFC 9562 which obsoletes it, give meaning to two positions in that string:

  • the version, the first character of the third group, is 1 to 8 (RFC 9562 section 4.2)
  • the variant, the first character of the fourth group, is 10xx, i.e. 8, 9, a or b (section 4.1)

The Nil UUID (section 5.9) and the Max UUID (section 5.10) are the only valid values that follow neither rule. Because neither position is checked, strings with an undefined version or a non-RFC variant are reported as valid UUIDs. string.tuuid (^[0-9a-fA-F]{32}$) has the same gap.

Steps to Reproduce

syntax = "proto3";
package repro;
import "buf/validate/validate.proto";

message Request {
  string id = 1 [(buf.validate.field).string.uuid = true];
}
import { create } from '@bufbuild/protobuf';
import { createValidator } from '@bufbuild/protovalidate';
import { RequestSchema } from './gen/repro_pb.js';

const validator = createValidator();
for (const id of [
  '919108f7-52d1-4320-9bac-f847db4148a8', // v4
  '919108f7-52d1-0320-9bac-f847db4148a8', // version 0
  '919108f7-52d1-f320-9bac-f847db4148a8', // version f
  '919108f7-52d1-4320-cbac-f847db4148a8', // variant c
  '919108f7-52d1-4320-0bac-f847db4148a8', // variant 0
]) {
  console.log(id, validator.validate(RequestSchema, create(RequestSchema, { id })).kind);
}

Expected Behavior

Only the v4 UUID is valid. The other four strings produce a string.uuid violation.

Actual Behavior

All five are valid:

919108f7-52d1-4320-9bac-f847db4148a8 valid
919108f7-52d1-0320-9bac-f847db4148a8 valid
919108f7-52d1-f320-9bac-f847db4148a8 valid
919108f7-52d1-4320-cbac-f847db4148a8 valid
919108f7-52d1-4320-0bac-f847db4148a8 valid

The rule is a CEL expression in validate.proto, so this is not specific to protovalidate-es. Every runtime evaluates the same expression.

Environment

  • Operating System: Windows 11
  • Version: 10.0.26200
  • Compiler/Toolchain: Node.js 24.19.0
  • Protobuf Compiler & Version: buf (npm @bufbuild/buf), protoc-gen-es
  • Protovalidate Version: buf.build/bufbuild/protovalidate main (commit 511051f7f4374c3ca873b53ae68a9288), @bufbuild/protovalidate 1.2.0

Possible Solution

Check the two positions in both rules, and keep Nil and Max valid. In RE2 syntax:

string.uuid:
^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|0{8}-0{4}-0{4}-0{4}-0{12}|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$

string.tuuid:
^(?:[0-9a-fA-F]{12}[1-8][0-9a-fA-F]{3}[89abAB][0-9a-fA-F]{15}|0{32}|[fF]{32})$

The conformance cases today only cover v1 to v5 and Nil as valid, so nothing pins the current behavior. New cases would cover v6, v7, v8 and Max as valid, and an out-of-range version and variant as invalid.

This makes the rules stricter, so values that pass today would start failing. If you would rather keep uuid as a shape check, the alternative is to change the documentation so it no longer says "as defined by RFC 4122".

Which of these two directions would you prefer? I'm happy to open a PR for either one, including the conformance cases.

Additional Context

For comparison, uuid (validate), validator (isUUID) and zod (z.uuid()) check both positions and accept Nil and Max.

Activity

  1. rodaine commented on Sep 15, 2026

    @rodaine
    Member

    Hey thanks for the report, @otnc! We'll likely want to keep the current rule as-is so that if/when a new version is introduced, we don't erroneously reject this new format. The documentation language should be updated to be clear it only enforces the shape of the types, not that it's indeed a fully-validated UUID.

    That said, a uuid_types or similar constraint could be added that takes a list of valid versions/variants that can be more strict:

    message Foo {
      string id = 1 [
        (buf.validate.field).string.uuid_types = UUID_V4
      ];
    
      string reference_id = 2 [
        (buf.validate.field).string = { uuid_types: [UUID_V4, UUID_V7] }
      ];
    }

    Just because of the complexity of the check, we'd likely push this down into a custom CEL function.

  2. otnc commented on Sep 15, 2026

    @otnc
    ContributorAuthor

    Thank you for the reply and explanation!
    That makes sense. I agree with keeping the current rule to support future UUID versions easily.
    Can I work on adding the ⁠uuid_types⁠ constraint and submit a PR for it? Let me know what you think about this idea!

  3. rodaine commented on Sep 15, 2026

    @rodaine
    Member

    Please do! We'll want a rule for both uuid_types and tuuid_types. Thanks for your help!

  4. otnc commented on Sep 15, 2026

    @otnc
    ContributorAuthor

    It's the middle of the night in my local time, so I'll get to it after I wake up!

  5. otnc commented on Sep 16, 2026

    @otnc
    ContributorAuthor

    Hi @rodaine . I made #532
    Please review when you have time!

  6. timostamm commented on Sep 23, 2026

    @timostamm
    Member

    Docs are fixed to clarify in #533.

    #534 makes the same clarification for (buf.validate.field).bytes.uuid, which should also be considered. Please feel free to open a feature request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions