✓ProveIt
Examples / Report #3293884 Saved analysis

Vulnerability Report: Local File Disclosure via file:// Protocol in cURL

curl · 2025-08-1013.8s analysis · $0.017 in model calls

What the report claims

The report claims that cURL's file:// protocol allows unauthorized local file system access, especially when run with elevated privileges, leading to disclosure of sensitive files like /etc/shadow.

Compare with curl's recorded outcome

Labeled AI slop

curl's maintainer lists this report as AI slop

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Needs human review

The available evidence does not settle the report. A maintainer should investigate.

curl policy: Tricking a user to run a command line
Judgment withdrawn. The answer failed the evidence check.
See the withdrawn model reasoning

The following reasoning failed the citation check and does not support the final finding.

  • The file:// protocol is an intentional, documented feature enabled by default; reading local files is its purpose.
  • The report's claim that file_do calls fopen on a raw path without validation is contradicted by the provided code: file_do uses a file descriptor opened during the connection phase (line 470).
  • Filesystem permissions are enforced by the OS at open time; path traversal and symlinks are standard filesystem behavior, not vulnerabilities.
  • The attack scenarios require the user to explicitly invoke curl with file:// URLs (or be tricked into doing so), which falls under the project's 'Tricking a user to run a command line' and 'Local attackers already present' non-security-issue categories.
  • Applications can disable file:// via CURLOPT_PROTOCOLS if they do not want local file access.
How this analysis ran 6 claims · 7 citations verified

Evidence check: judge quoted an external source that does not say that (https://lists.gnu.org/archive/html/gnunet-svn/2017-02/msg00651.html)

Checked against curl master on 2025-08-10 (626051cd8bed). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 4 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
6 claims · 1564 tokens · $0.0002
3 · Evidence
2 code snippets
real curl source at 626051cd8bed · 1 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
11405 tokens · 10.6s · $0.0165
5 · Receipts
7 of 8 citations verified
every citation must point at code the judge was shown
Inspect the withdrawn draft reply

This draft belongs to the withdrawn judgment. It may contain unsupported claims.

The file:// protocol is a documented feature, not a vulnerability. It reads local files the calling process has permission to access—exactly as designed. The code shows file_do uses an already-opened file descriptor (lib/file.c:470), not a raw fopen on the URL path. Filesystem permissions are enforced by the OS. Running curl with sudo gives it root access; that is a system configuration choice, not a curl flaw. Applications can restrict protocols via CURLOPT_PROTOCOLS. This report describes intended behavior.

Inspect all claims and source code 6 claims · 2 code excerpts

This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.

Report claims

C1flaw supported

The file handler is enabled by default in the protocols array

Read the reasoning

The file:// protocol handler is intentionally enabled by default. This is a documented feature, not a flaw.

lib/url.c:0
C2flaw contradicted

The file_do function reads the raw path from data->state.up.path and opens it with fopen without validation

Read the reasoning

The provided file_do code (lines 443-603) reads from an already-opened file descriptor (file->fd), not by calling fopen on the raw path. The fopen occurs in the connection phase (file_connect), which is not shown but is where path parsing happens.

C3flaw contradicted

No validation of path traversal sequences, symbolic links, or filesystem permissions

Read the reasoning

Filesystem permissions are validated by the OS when the file is opened (in the connection phase). Path traversal (../) and symlinks are standard filesystem semantics, not vulnerabilities. The handler follows RFC 1738 for file:// URLs.

C4trigger supported

curl "file:///etc/passwd" reads system user info

Read the reasoning

curl 'file:///etc/passwd' works as intended: the file:// protocol reads local files the user has permission to access. This is documented functionality.

C5trigger supported

sudo curl "file:///etc/shadow" exposes password hashes

Read the reasoning

Running curl with sudo gives it root privileges; reading /etc/shadow is then permitted by the OS. This is not a curl vulnerability—it is how Unix permissions work.

C6reproduction supported

echo "test" > /tmp/test_file && curl "file:///tmp/test_file"

Read the reasoning

The reproduction demonstrates the file:// protocol working as designed for a user-writable file.

What the report says about other projects

confirmed CURLOPT_PROTOCOLS restricts which protocols can be used

“CURLOPT_PROTOCOLS restricts which protocols can be used” source

Read the original report

Full report on HackerOne ↗

Summary A security vulnerability has been identified that allows unauthorized local file system access via the file:// protocol in cURL, particularly when executed with elevated privileges (e.g., sudo). This could lead to sensitive data exposure, including password hashes stored in /etc/shadow. Steps to Reproduce Locating Protocol Handlers First, I searched for protocol registration in cURL's source: // Found in lib/url.c static const struct Curl_handler * const protocols[] = { &Curl_handler_dict, &Curl_handler_file, // ← This is what we're interested in &Curl_handler_ftp, ... }; Key Insight: The file handler is enabled by default in the protocols array. Examining the File Protocol Handler I traced the file handler implementation: // lib/file.c static CURLcode file_do(struct Curl_easy *data, bool *done) { char *path = data->state.up.path; // ← Raw path from URL FILE *file = fopen(path, "rb"); // ← Direct filesystem access ... } Vulnerability Found: No validation of: Path traversal sequences (../) Symbolic links Filesystem permissions 1. Basic Local File Read curl "file:///etc/passwd" # Reads system user info 2. Privileged File Access (with sudo) sudo curl "file:///etc/shadow" # Exposes password hashes echo "test" > /tmp/test_file curl "file:///tmp/test_file" sudo curl "file:///etc/shadow" # Expected Output: root:*: kali:$ [...] References cURL Security Documentation - Official security considerations for cURL CURLOPT_PROTOCOLS man page - Protocol restriction options ## Impact Confidentiality Breach: Read access to sensitive system files Privilege Escalation: Potential root access via hash cracking Business Impact: Critical for systems processing sensitive data Often missed in containerized environments

Source code

lib/file.clines 443–603 · definition of file_do (truncated)
443static CURLcode file_do(struct Curl_easy *data, bool *done)
C2: file_do function definition shows no fopen call; the report's quoted line 'FILE *file = fopen(path, "rb");' does not appear in this function
C4: file_do implements reading of local files via file:// protocol as designed
C5: file_do reads any file the process can access; when run via sudo, the process has root permissions
C6: file_do reads local files; the reproduction shows normal file:// usage
444{
445 /* This implementation ignores the hostname in conformance with
C3: Comment states implementation follows RFC 1738: 'Only local files (reachable via the standard file system) are supported'
446 RFC 1738. Only local files (reachable via the standard file system)
447 are supported. This means that files on remotely mounted directories
448 (via NFS, Samba, NT sharing) can be accessed through a file:// URL
449 */
450 struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
451 CURLcode result = CURLE_OK;
452 struct_stat statbuf; /* struct_stat instead of struct stat just to allow the
453 Windows version to have a different struct without
454 having to redefine the simple word 'stat' */
455 curl_off_t expected_size = -1;
456 bool size_known;
457 bool fstated = FALSE;
458 int fd;
459 char *xfer_buf;
460 size_t xfer_blen;
461
462 *done = TRUE; /* unconditionally */
463 if(!file)
464 return CURLE_FAILED_INIT;
465
466 if(data->state.upload)
467 return file_upload(data, file);
468
469 /* get the fd from the connection phase */
470 fd = file->fd;
C2: file_do uses fd = file->fd (opened during connection phase), not fopen on raw path
471
472 /* VMS: This only works reliable for STREAMLF files */
473 if(fstat(fd, &statbuf) != -1) {
C3: fstat on the opened fd validates the file exists and is accessible; filesystem permissions are enforced by the OS at open time
474 if(!S_ISDIR(statbuf.st_mode))
475 expected_size = statbuf.st_size;
476 /* and store the modification time */
477 data->info.filetime = statbuf.st_mtime;
478 fstated = TRUE;
479 }
480
481 if(fstated && !data->state.range && data->set.timecondition &&
482 !Curl_meets_timecondition(data, data->info.filetime))
483 return CURLE_OK;
484
485 if(fstated) {
486 time_t filetime;
487 struct tm buffer;
488 const struct tm *tm = &buffer;
489 char header[80];
490 int headerlen;
491 static const char accept_ranges[]= { "Accept-ranges: bytes\r\n" };
492 if(expected_size >= 0) {
493 headerlen =
494 msnprintf(header, sizeof(header), "Content-Length: %" FMT_OFF_T "\r\n",
495 expected_size);
496 result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
497 if(result)
498 return result;
499
500 result = Curl_client_write(data, CLIENTWRITE_HEADER,
501 accept_ranges, sizeof(accept_ranges) - 1);
502 if(result != CURLE_OK)
503 return result;
504 }
505
506 filetime = (time_t)statbuf.st_mtime;
507 result = Curl_gmtime(filetime, &buffer);
508 if(result)
509 return result;
510
511 /* format: "Tue, 15 Nov 1994 12:45:26 GMT" */
512 headerlen =
513 msnprintf(header, sizeof(header),
514 "Last-Modified: %s, %02d %s %4d %02d:%02d:%02d GMT\r\n",
515 Curl_wkday[tm->tm_wday ? tm->tm_wday-1 : 6],
516 tm->tm_mday,
517 Curl_month[tm->tm_mon],
518 tm->tm_year + 1900,
519 tm->tm_hour,
520 tm->tm_min,
521 tm->tm_sec);
522 result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
523 if(!result)
524 /* end of headers */
525 result = Curl_client_write(data, CLIENTWRITE_HEADER, "\r\n", 2);
526 if(result)
527 return result;
528 /* set the file size to make it available post transfer */
529 Curl_pgrsSetDownloadSize(data, expected_size);
530 if(data->req.no_body)
531 return CURLE_OK;
532 }
533
534 /* Check whether file range has been specified */
535 result = Curl_range(data);
536 if(result)
537 return result;
538
539 /* Adjust the start offset in case we want to get the N last bytes
540 * of the stream if the filesize could be determined */
541 if(data->state.resume_from < 0) {
542 if(!fstated) {
543 failf(data, "cannot get the size of file.");
544 return CURLE_READ_ERROR;
545 }
546 data->state.resume_from += (curl_off_t)statbuf.st_size;
547 }
548
549 if(data->state.resume_from > 0) {
550 /* We check explicitly if we have a start offset, because
551 * expected_size may be -1 if we do not know how large the file is,
552 * in which case we should not adjust it. */
553 if(data->state.resume_from <= expected_size)
554 expected_size -= data->state.resume_from;
555 else {
556 failf(data, "failed to resume file:// transfer");
557 return CURLE_BAD_DOWNLOAD_RESUME;
558 }
559 }
560
561 /* A high water mark has been specified so we obey... */
562 if(data->req.maxdownload > 0)
563 expected_size = data->req.maxdownload;
564
565 if(!fstated || (expected_size <= 0))
566 size_known = FALSE;
567 else
568 size_known = TRUE;
569
570 /* The following is a shortcut implementation of file reading
571 this is both more efficient than the former call to download() and
572 it avoids problems with select() and recv() on file descriptors
573 in Winsock */
574 if(size_known)
575 Curl_pgrsSetDownloadSize(data, expected_size);
576
577 if(data->state.resume_from) {
578 if(!S_ISDIR(statbuf.st_mode)) {
579#if defined(__AMIGA__) || defined(__MINGW32CE__)
580 if(data->state.resume_from !=
581 lseek(fd, (off_t)data->state.resume_from, SEEK_SET))
582#else
583 if(data->state.resume_from !=
584 lseek(fd, data->state.resume_from, SEEK_SET))
585#endif
586 return CURLE_BAD_DOWNLOAD_RESUME;
587 }
588 else {
589 return CURLE_BAD_DOWNLOAD_RESUME;
590 }
591 }
592
593 result = Curl_multi_xfer_buf_borrow(data, &xfer_buf, &xfer_blen);
594 if(result)
595 goto out;
596
597 if(!S_ISDIR(statbuf.st_mode)) {
598 while(!result) {
599 ssize_t nread;
600 /* Do not fill a whole buffer if we want less than all data */
601 size_t bytestoread;
602
603 if(size_known) {
lib/file.c@curl-8_15_0lines 443–603 · definition of file_do as in release curl-8_15_0; differs from master
443static CURLcode file_do(struct Curl_easy *data, bool *done)
444{
445 /* This implementation ignores the hostname in conformance with
446 RFC 1738. Only local files (reachable via the standard file system)
447 are supported. This means that files on remotely mounted directories
448 (via NFS, Samba, NT sharing) can be accessed through a file:// URL
449 */
450 struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
451 CURLcode result = CURLE_OK;
452 struct_stat statbuf; /* struct_stat instead of struct stat just to allow the
453 Windows version to have a different struct without
454 having to redefine the simple word 'stat' */
455 curl_off_t expected_size = -1;
456 bool size_known;
457 bool fstated = FALSE;
458 int fd;
459 char *xfer_buf;
460 size_t xfer_blen;
461
462 *done = TRUE; /* unconditionally */
463 if(!file)
464 return CURLE_FAILED_INIT;
465
466 if(data->state.upload)
467 return file_upload(data, file);
468
469 /* get the fd from the connection phase */
470 fd = file->fd;
471
472 /* VMS: This only works reliable for STREAMLF files */
473 if(-1 != fstat(fd, &statbuf)) {
474 if(!S_ISDIR(statbuf.st_mode))
475 expected_size = statbuf.st_size;
476 /* and store the modification time */
477 data->info.filetime = statbuf.st_mtime;
478 fstated = TRUE;
479 }
480
481 if(fstated && !data->state.range && data->set.timecondition &&
482 !Curl_meets_timecondition(data, data->info.filetime))
483 return CURLE_OK;
484
485 if(fstated) {
486 time_t filetime;
487 struct tm buffer;
488 const struct tm *tm = &buffer;
489 char header[80];
490 int headerlen;
491 static const char accept_ranges[]= { "Accept-ranges: bytes\r\n" };
492 if(expected_size >= 0) {
493 headerlen =
494 msnprintf(header, sizeof(header), "Content-Length: %" FMT_OFF_T "\r\n",
495 expected_size);
496 result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
497 if(result)
498 return result;
499
500 result = Curl_client_write(data, CLIENTWRITE_HEADER,
501 accept_ranges, sizeof(accept_ranges) - 1);
502 if(result != CURLE_OK)
503 return result;
504 }
505
506 filetime = (time_t)statbuf.st_mtime;
507 result = Curl_gmtime(filetime, &buffer);
508 if(result)
509 return result;
510
511 /* format: "Tue, 15 Nov 1994 12:45:26 GMT" */
512 headerlen =
513 msnprintf(header, sizeof(header),
514 "Last-Modified: %s, %02d %s %4d %02d:%02d:%02d GMT\r\n",
515 Curl_wkday[tm->tm_wday ? tm->tm_wday-1 : 6],
516 tm->tm_mday,
517 Curl_month[tm->tm_mon],
518 tm->tm_year + 1900,
519 tm->tm_hour,
520 tm->tm_min,
521 tm->tm_sec);
522 result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
523 if(!result)
524 /* end of headers */
525 result = Curl_client_write(data, CLIENTWRITE_HEADER, "\r\n", 2);
526 if(result)
527 return result;
528 /* set the file size to make it available post transfer */
529 Curl_pgrsSetDownloadSize(data, expected_size);
530 if(data->req.no_body)
531 return CURLE_OK;
532 }
533
534 /* Check whether file range has been specified */
535 result = Curl_range(data);
536 if(result)
537 return result;
538
539 /* Adjust the start offset in case we want to get the N last bytes
540 * of the stream if the filesize could be determined */
541 if(data->state.resume_from < 0) {
542 if(!fstated) {
543 failf(data, "cannot get the size of file.");
544 return CURLE_READ_ERROR;
545 }
546 data->state.resume_from += (curl_off_t)statbuf.st_size;
547 }
548
549 if(data->state.resume_from > 0) {
550 /* We check explicitly if we have a start offset, because
551 * expected_size may be -1 if we do not know how large the file is,
552 * in which case we should not adjust it. */
553 if(data->state.resume_from <= expected_size)
554 expected_size -= data->state.resume_from;
555 else {
556 failf(data, "failed to resume file:// transfer");
557 return CURLE_BAD_DOWNLOAD_RESUME;
558 }
559 }
560
561 /* A high water mark has been specified so we obey... */
562 if(data->req.maxdownload > 0)
563 expected_size = data->req.maxdownload;
564
565 if(!fstated || (expected_size <= 0))
566 size_known = FALSE;
567 else
568 size_known = TRUE;
569
570 /* The following is a shortcut implementation of file reading
571 this is both more efficient than the former call to download() and
572 it avoids problems with select() and recv() on file descriptors
573 in Winsock */
574 if(size_known)
575 Curl_pgrsSetDownloadSize(data, expected_size);
576
577 if(data->state.resume_from) {
578 if(!S_ISDIR(statbuf.st_mode)) {
579#if defined(__AMIGA__) || defined(__MINGW32CE__)
580 if(data->state.resume_from !=
581 lseek(fd, (off_t)data->state.resume_from, SEEK_SET))
582#else
583 if(data->state.resume_from !=
584 lseek(fd, data->state.resume_from, SEEK_SET))
585#endif
586 return CURLE_BAD_DOWNLOAD_RESUME;
587 }
588 else {
589 return CURLE_BAD_DOWNLOAD_RESUME;
590 }
591 }
592
593 result = Curl_multi_xfer_buf_borrow(data, &xfer_buf, &xfer_blen);
594 if(result)
595 goto out;
596
597 if(!S_ISDIR(statbuf.st_mode)) {
598 while(!result) {
599 ssize_t nread;
600 /* Do not fill a whole buffer if we want less than all data */
601 size_t bytestoread;
602
603 if(size_known) {