diff options
| author | jsing <> | 2026-07-14 15:52:52 +0000 |
|---|---|---|
| committer | jsing <> | 2026-07-14 15:52:52 +0000 |
| commit | d9a371b178de7043470c89bef81aa96da20dc855 (patch) | |
| tree | 0c1869d6a4831f89be1d5d75189412787ded0f50 /src | |
| parent | 830c372f670c7dc9435cd7c2dd13364913f22b52 (diff) | |
| download | openbsd-d9a371b178de7043470c89bef81aa96da20dc855.tar.gz openbsd-d9a371b178de7043470c89bef81aa96da20dc855.tar.bz2 openbsd-d9a371b178de7043470c89bef81aa96da20dc855.zip | |
Fix X.509 constraints URI host parsing.
An authority in a URI is only terminated by a slash, question mark or hash,
however the current code also included colons. This allows a specically
crafted userinfo to bypass name constraints host checks. Additionally, IPv6
literals may only be specified when enclosed with square brackets, which is
not enforced.
Rewrite parts of the host and IP parsing code to be more strict, fixing
both of these issues in the process.
Thanks to Jack Lloyd for reporting the userinfo bypass.
ok tb@
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/libcrypto/x509/x509_constraints.c | 101 |
1 files changed, 72 insertions, 29 deletions
diff --git a/src/lib/libcrypto/x509/x509_constraints.c b/src/lib/libcrypto/x509/x509_constraints.c index c4f32c9cfc..61a08cacef 100644 --- a/src/lib/libcrypto/x509/x509_constraints.c +++ b/src/lib/libcrypto/x509/x509_constraints.c | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* $OpenBSD: x509_constraints.c,v 1.33 2026/04/13 17:04:23 beck Exp $ */ | 1 | /* $OpenBSD: x509_constraints.c,v 1.34 2026/07/14 15:52:52 jsing Exp $ */ |
| 2 | /* | 2 | /* |
| 3 | * Copyright (c) 2020 Bob Beck <beck@openbsd.org> | 3 | * Copyright (c) 2020 Bob Beck <beck@openbsd.org> |
| 4 | * | 4 | * |
| @@ -38,21 +38,40 @@ | |||
| 38 | #define MAX_IP_ADDRESS_LENGTH (size_t)46 | 38 | #define MAX_IP_ADDRESS_LENGTH (size_t)46 |
| 39 | 39 | ||
| 40 | static int | 40 | static int |
| 41 | cbs_is_ip_address(CBS *cbs, int *is_ip) | 41 | host_is_ip_address(CBS *cbs, int *is_ip) |
| 42 | { | 42 | { |
| 43 | struct sockaddr_in6 sin6; | 43 | struct sockaddr_in6 sin6; |
| 44 | struct sockaddr_in sin4; | 44 | struct sockaddr_in sin4; |
| 45 | uint8_t first, last; | ||
| 45 | char *name = NULL; | 46 | char *name = NULL; |
| 47 | CBS ipv6_cbs; | ||
| 46 | 48 | ||
| 47 | *is_ip = 0; | 49 | *is_ip = 0; |
| 50 | |||
| 48 | if (CBS_len(cbs) > MAX_IP_ADDRESS_LENGTH) | 51 | if (CBS_len(cbs) > MAX_IP_ADDRESS_LENGTH) |
| 49 | return 1; | 52 | return 1; |
| 53 | |||
| 54 | /* Must be an IPv6 literal. */ | ||
| 55 | CBS_dup(cbs, &ipv6_cbs); | ||
| 56 | if (!CBS_get_u8(&ipv6_cbs, &first)) | ||
| 57 | return 1; | ||
| 58 | if (!CBS_get_last_u8(&ipv6_cbs, &last)) | ||
| 59 | return 1; | ||
| 60 | if (first == '[' && last == ']') { | ||
| 61 | if (!CBS_strdup(&ipv6_cbs, &name)) | ||
| 62 | return 0; | ||
| 63 | if (inet_pton(AF_INET6, name, &sin6) == 1) | ||
| 64 | *is_ip = 1; | ||
| 65 | goto done; | ||
| 66 | } | ||
| 67 | |||
| 68 | /* Or an IPv4 address. */ | ||
| 50 | if (!CBS_strdup(cbs, &name)) | 69 | if (!CBS_strdup(cbs, &name)) |
| 51 | return 0; | 70 | return 0; |
| 52 | if (inet_pton(AF_INET, name, &sin4) == 1 || | 71 | if (inet_pton(AF_INET, name, &sin4) == 1) |
| 53 | inet_pton(AF_INET6, name, &sin6) == 1) | ||
| 54 | *is_ip = 1; | 72 | *is_ip = 1; |
| 55 | 73 | ||
| 74 | done: | ||
| 56 | free(name); | 75 | free(name); |
| 57 | return 1; | 76 | return 1; |
| 58 | } | 77 | } |
| @@ -273,13 +292,13 @@ x509_constraints_valid_host(CBS *cbs, int permit_ip) | |||
| 273 | return 0; | 292 | return 0; |
| 274 | if (first == '.') | 293 | if (first == '.') |
| 275 | return 0; /* leading . not allowed in a host name or IP */ | 294 | return 0; /* leading . not allowed in a host name or IP */ |
| 276 | if (!permit_ip) { | 295 | if (!host_is_ip_address(cbs, &is_ip)) |
| 277 | if (!cbs_is_ip_address(cbs, &is_ip)) | 296 | return 0; |
| 278 | return 0; | 297 | if (is_ip) { |
| 279 | if (is_ip) | 298 | if (permit_ip) |
| 280 | return 0; | 299 | return 1; |
| 300 | return 0; | ||
| 281 | } | 301 | } |
| 282 | |||
| 283 | return x509_constraints_valid_domain_internal(cbs, 0); | 302 | return x509_constraints_valid_domain_internal(cbs, 0); |
| 284 | } | 303 | } |
| 285 | 304 | ||
| @@ -508,10 +527,11 @@ x509_constraints_valid_domain_constraint(CBS *cbs) | |||
| 508 | int | 527 | int |
| 509 | x509_constraints_uri_host(uint8_t *uri, size_t len, char **hostpart) | 528 | x509_constraints_uri_host(uint8_t *uri, size_t len, char **hostpart) |
| 510 | { | 529 | { |
| 511 | size_t i, hostlen = 0; | ||
| 512 | uint8_t *authority = NULL; | 530 | uint8_t *authority = NULL; |
| 513 | char *host = NULL; | 531 | uint8_t *host = NULL; |
| 532 | size_t hostlen = 0; | ||
| 514 | CBS host_cbs; | 533 | CBS host_cbs; |
| 534 | size_t i; | ||
| 515 | 535 | ||
| 516 | /* | 536 | /* |
| 517 | * Find first '//'. there must be at least a '//' and | 537 | * Find first '//'. there must be at least a '//' and |
| @@ -542,31 +562,55 @@ x509_constraints_uri_host(uint8_t *uri, size_t len, char **hostpart) | |||
| 542 | for (i = authority - uri; i < len; i++) { | 562 | for (i = authority - uri; i < len; i++) { |
| 543 | if (!isascii(uri[i])) | 563 | if (!isascii(uri[i])) |
| 544 | return 0; | 564 | return 0; |
| 545 | /* it has a userinfo part */ | 565 | /* |
| 546 | if (uri[i] == '@') { | 566 | * Per RFC 3986 section 3.2, authority is terminated |
| 547 | hostlen = 0; | 567 | * by a slash (/), question mark (?), hash (#) or by |
| 548 | /* it can only have one */ | 568 | * the end of the URI. |
| 549 | if (host != NULL) | 569 | */ |
| 550 | break; | 570 | if (uri[i] == '/' || uri[i] == '?' || uri[i] == '#') |
| 551 | /* start after the userinfo part */ | ||
| 552 | host = uri + i + 1; | ||
| 553 | continue; | ||
| 554 | } | ||
| 555 | /* did we find the end? */ | ||
| 556 | if (uri[i] == ':' || uri[i] == '/' || uri[i] == '?' || | ||
| 557 | uri[i] == '#') | ||
| 558 | break; | 571 | break; |
| 572 | |||
| 559 | hostlen++; | 573 | hostlen++; |
| 560 | } | 574 | } |
| 575 | |||
| 576 | host = authority; | ||
| 577 | |||
| 578 | /* Remove any leading userinfo part. */ | ||
| 579 | for (i = 0; i < hostlen; i++) { | ||
| 580 | if (authority[i] != '@') | ||
| 581 | continue; | ||
| 582 | |||
| 583 | /* Only one at-sign (@) is permitted. */ | ||
| 584 | if (host != authority) | ||
| 585 | return 0; | ||
| 586 | |||
| 587 | /* Start after the userinfo part. */ | ||
| 588 | host = authority + i + 1; | ||
| 589 | } | ||
| 590 | hostlen = hostlen - (host - authority); | ||
| 591 | |||
| 561 | if (hostlen == 0) | 592 | if (hostlen == 0) |
| 562 | return 0; | 593 | return 0; |
| 563 | if (host == NULL) | 594 | |
| 564 | host = authority; | 595 | /* Remove any port part, respecting IPv6 literals. */ |
| 596 | for (i = hostlen - 1; i > 0; i--) { | ||
| 597 | if (host[i] == ']') | ||
| 598 | break; | ||
| 599 | if (host[i] == ':') { | ||
| 600 | hostlen = i; | ||
| 601 | break; | ||
| 602 | } | ||
| 603 | } | ||
| 604 | |||
| 605 | if (hostlen == 0) | ||
| 606 | return 0; | ||
| 607 | |||
| 565 | CBS_init(&host_cbs, host, hostlen); | 608 | CBS_init(&host_cbs, host, hostlen); |
| 566 | if (!x509_constraints_valid_host(&host_cbs, 1)) | 609 | if (!x509_constraints_valid_host(&host_cbs, 1)) |
| 567 | return 0; | 610 | return 0; |
| 568 | if (hostpart != NULL && !CBS_strdup(&host_cbs, hostpart)) | 611 | if (hostpart != NULL && !CBS_strdup(&host_cbs, hostpart)) |
| 569 | return 0; | 612 | return 0; |
| 613 | |||
| 570 | return 1; | 614 | return 1; |
| 571 | } | 615 | } |
| 572 | 616 | ||
| @@ -647,8 +691,7 @@ x509_constraints_domain(char *domain, size_t dlen, char *constraint, size_t len) | |||
| 647 | 691 | ||
| 648 | int | 692 | int |
| 649 | x509_constraints_uri(uint8_t *uri, size_t ulen, uint8_t *constraint, | 693 | x509_constraints_uri(uint8_t *uri, size_t ulen, uint8_t *constraint, |
| 650 | size_t len, | 694 | size_t len, int *error) |
| 651 | int *error) | ||
| 652 | { | 695 | { |
| 653 | int ret = 0; | 696 | int ret = 0; |
| 654 | char *hostpart = NULL; | 697 | char *hostpart = NULL; |
