HexBrain

curl · Report #3598358

urlapi: off-by-one in custom scheme validation skips last character

Informativestatus
highseverity
38upvotes
12 Mar 2026disclosed

How it was found

Summary

In lib/urlapi.c, the set_url_scheme() function has an off-by-one error when validating custom scheme names. The validation loop checks scheme[0] twice (once by ISALPHA, once in the loop) and never checks the last character. This allows schemes ending with any arbitrary byte (e.g., foo!, bar{, bad/) to pass validation when set via curl_url_set(url, CURLUPART_SCHEME, ..., CURLU_NON_SUPPORT_SCHEME), violating RFC 3986 Section 3.1.

Affected version

curl 8.19.1-DEV (Darwin) libcurl/8.19.1-DEV OpenSSL/3.6.1 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns mqtt mqtts pop3 pop3s rtsp smb smbs smtp smtps telnet tftp ws wss Features: alt-svc AsynchDNS brotli HSTS HTTPS-proxy IDN IPv6 Largefile libz NTLM SSL threadsafe TLS-SRP UnixSockets zstd

Tested on macOS (Darwin 25.2.0, arm64). The bug is platform-independent, in lib/urlapi.c lines 1648-1657.

Steps To Reproduce

Build and run this PoC:

#include <stdio.h>
#include <curl/curl.h>

int main(void) {
  CURLU *url;
  CURLUcode rc;
  const char *schemes[] = {"bad!", "bad{", "bad/", "bad\\", "a!", NULL};

  for(int i = 0; schemes[i]; i++) {
    url = curl_url();
    rc = curl_url_set(url, CURLUPART_SCHEME, schemes[i],
                      CURLU_NON_SUPPORT_SCHEME);
    char *out = NULL;
    if(rc == CURLUE_OK)
      curl_url_get(url, CURLUPART_SCHEME, &out, 0);
    printf("%-10s  %s", schemes[i],
           rc == CURLUE_OK ? "ACCEPTED (BUG!)" : "REJECTED (correct)");
    if(out) { printf("  stored='%s'", out); curl_free(out); }
    printf("\n");
    curl_url_cleanup(url);
  }
  return 0;
}

Output:

bad! ACCEPTED (BUG!) stored='bad!' bad{ ACCEPTED (BUG!) stored='bad{' bad/ ACCEPTED (BUG!) stored='bad/' bad\ ACCEPTED (BUG!) stored='bad' a! ACCEPTED (BUG!) stored='a!' All should be REJECTED per RFC 3986 Section 3.1.

Root cause in set_url_scheme() (urlapi.c:1648-1657): after the ISALPHA(*s) check, s is not incremented before entering the while(--plen) loop, so the first character is validated twice and the last character is never validated.

Suggested fix: add s++ after the ISALPHA(*s) check, before the loop.

Impact

Summary

  • Input validation bypass: Applications using CURLU_NON_SUPPORT_SCHEME that rely on libcurl's URL API for scheme validation will accept malformed schemes containing illegal characters in the final position.
  • Potential SSRF filter bypass: Security filters that use libcurl's URL parser to validate/normalize URLs before making requests may accept URLs with invalid scheme characters, which downstream components may interpret differently.
  • RFC non-compliance: Violates RFC 3986 Section 3.1 scheme syntax: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )

Note: The bug is only triggerable via the direct CURLUPART_SCHEME API, not when parsing a full URL via CURLUPART_URL (because the URL parser's scheme extraction filters special characters before reaching set_url_scheme()).

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original