summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2026-06-09 22:35:20 -0500
committerLudovic Courtès <ludo@gnu.org>2026-07-02 19:42:47 +0200
commitfc06ada0602983350646c741531ec2b1887f3407 (patch)
tree6189efa43a32ae9968931984d187b0b6b0364a8d
parented0a9721f8a20d6ddcf6a0495302f502b3f7bb17 (diff)
scripts: substitute: restrict where "file://" URIs can be used [security fix].
There are two potential places where file:// URIs could be used in 'guix substitute': 1. The substitute urls, which are used for fetching narinfos. These can come from the daemon, via daemon option "substitute-urls", from the client, via daemon option "untrusted-substitute-urls", or from 'guix discover'. The latter two must not be allowed to provide file:// URIs, since that could be used to cause the daemon to read private files, files in /proc or /dev, etc, and various parts of those files may show up in the backtrace from 'guix substitute'. 2. The narinfos themselves, which may offer arbitrarily many URIs to download the nar of the store item in question. These URIs do not need to be in the signed portion of the narinfo, so they may be attacker-controlled. These should also not be allowed to provide file:// URIs, with the sole exception being in the case of the test suite for the sake of simplifying the tests. To accomplish this, we error out of 'guix substitute' if "untrusted-substitute-urls" includes a file:// URI, and we filter out all file:// URIs from those provided by 'guix discover'. We also ignore all narinfos containing file:// URIs unless '%allow-unsafe-substitute-uris?' is #t. * guix/scripts/substitute.scm (%allow-unsafe-substitute-uris?, %allow-unsafe-narinfo-uris?): new parameters. (process-substitution, process-substitution/fallback): honor '%allow-unsafe-substitute-uris?'. (uri-safe?, narinfo-uris-safe?, assert-safe-uris): new procedures. (%default-substitute-urls): use assert-safe-uris for the "untrusted-substitute-urls" daemon option. (%local-substitute-urls): filter out file:// URIs. * tests/substitute.scm ("file:// URI prohibited by default"): new test. Set '%allow-unsafe-substitute-uris?' to #t after it finishes. Change-Id: I34d7e58b5a01c7cc61b52fcd61b547bba2ce4300 Signed-off-by: Ludovic Courtès <ludo@gnu.org>
-rwxr-xr-xguix/scripts/substitute.scm115
-rw-r--r--tests/substitute.scm20
2 files changed, 100 insertions, 35 deletions
diff --git a/guix/scripts/substitute.scm b/guix/scripts/substitute.scm
index 455f5a23461..49f5389fe27 100755
--- a/guix/scripts/substitute.scm
+++ b/guix/scripts/substitute.scm
@@ -58,6 +58,7 @@
58 #:use-module (web response) 58 #:use-module (web response)
59 #:use-module (guix http-client) 59 #:use-module (guix http-client)
60 #:export (%allow-unauthenticated-substitutes? 60 #:export (%allow-unauthenticated-substitutes?
61 %allow-unsafe-substitute-uris?
61 %reply-file-descriptor 62 %reply-file-descriptor
62 63
63 substitute-urls 64 substitute-urls
@@ -90,6 +91,19 @@ disabled!~%"))
90 (and=> (getenv "GUIX_ALLOW_UNAUTHENTICATED_SUBSTITUTES") 91 (and=> (getenv "GUIX_ALLOW_UNAUTHENTICATED_SUBSTITUTES")
91 (cut string-ci=? <> "yes")))) 92 (cut string-ci=? <> "yes"))))
92 93
94(define %allow-unsafe-substitute-uris?
95 ;; Whether to allow substitutes with a "file://" URI. These are useful for
96 ;; testing purposes but should never be allowed otherwise, as the URIs
97 ;; contained in narinfos are not signed and could point to any
98 ;; attacker-controlled location. Use GUIX_ALLOW_UNAUTHENTICATED_SUBSTITUTES
99 ;; as a way to tell when we're testing.
100 (make-parameter (%allow-unauthenticated-substitutes?)))
101
102(define %allow-unsafe-narinfo-uris?
103 ;; Like %allow-unsafe-substitute-uris?, but affecting the urls used to fetch
104 ;; narinfos.
105 (make-parameter (%allow-unauthenticated-substitutes?)))
106
93(define (at-most max-length lst) 107(define (at-most max-length lst)
94 "If LST is shorter than MAX-LENGTH, return it and the empty list; otherwise 108 "If LST is shorter than MAX-LENGTH, return it and the empty list; otherwise
95return its MAX-LENGTH first elements and its tail." 109return its MAX-LENGTH first elements and its tail."
@@ -368,6 +382,21 @@ server certificates."
368 (drain-input socket) 382 (drain-input socket)
369 socket)))))))) 383 socket))))))))
370 384
385(define (uri-safe? uri)
386 "Return a boolean indicating whether URI, which is either a uri or its
387string representation, is generally safe to use without requiring any trust."
388 (let ((uri (if (uri? uri)
389 uri
390 (string->uri uri))))
391 (and uri
392 (case (uri-scheme uri)
393 ((#f file) #f)
394 (else #t)))))
395
396(define (narinfo-uris-safe? narinfo)
397 (or (%allow-unsafe-substitute-uris?)
398 (every uri-safe? (narinfo-uris narinfo))))
399
371(define* (process-substitution/fallback narinfo destination 400(define* (process-substitution/fallback narinfo destination
372 #:key cache-urls acl 401 #:key cache-urls acl
373 deduplicate? print-build-trace? 402 deduplicate? print-build-trace?
@@ -391,9 +420,10 @@ way to download the nar."
391 #:open-connection 420 #:open-connection
392 open-connection-for-uri/cached) 421 open-connection-for-uri/cached)
393 ((alternate) 422 ((alternate)
394 (if (or (equivalent-narinfo? narinfo alternate) 423 (if (and (narinfo-uris-safe? alternate)
395 (valid-narinfo? alternate acl) 424 (or (equivalent-narinfo? narinfo alternate)
396 (%allow-unauthenticated-substitutes?)) 425 (valid-narinfo? alternate acl)
426 (%allow-unauthenticated-substitutes?)))
397 (guard (c ((or (http-get-error? c) 427 (guard (c ((or (http-get-error? c)
398 (network-error? c)) 428 (network-error? c))
399 (when (http-get-error? c) 429 (when (http-get-error? c)
@@ -429,6 +459,16 @@ PORT."
429 (const #t) 459 (const #t)
430 (cut valid-narinfo? <> acl)))) 460 (cut valid-narinfo? <> acl))))
431 461
462 (define (fallback)
463 (process-substitution/fallback narinfo destination
464 #:cache-urls cache-urls
465 #:acl acl
466 #:deduplicate? deduplicate?
467 #:print-build-trace?
468 print-build-trace?
469 #:fast-decompression?
470 fast-decompression?))
471
432 (unless narinfo 472 (unless narinfo
433 (raise 473 (raise
434 (formatted-message 474 (formatted-message
@@ -437,33 +477,30 @@ PORT."
437 477
438 (let ((expected-hash 478 (let ((expected-hash
439 actual-hash 479 actual-hash
440 (guard 480 (cond
441 (c ((or (http-get-error? c) 481 ((narinfo-uris-safe? narinfo)
442 (network-error? c)) 482 (guard
443 (when (http-get-error? c) 483 (c ((or (http-get-error? c)
444 (warning (G_ "download from '~a' failed: ~a, ~s~%") 484 (network-error? c))
445 (uri->string (http-get-error-uri c)) 485 (when (http-get-error? c)
446 (http-get-error-code c) 486 (warning (G_ "download from '~a' failed: ~a, ~s~%")
447 (http-get-error-reason c))) 487 (uri->string (http-get-error-uri c))
448 (format 488 (http-get-error-code c)
449 (current-error-port) 489 (http-get-error-reason c)))
450 (G_ "retrying download of '~a' with other substitute URLs...~%") 490 (format
451 store-item) 491 (current-error-port)
452 (process-substitution/fallback narinfo destination 492 (G_ "retrying download of '~a' with other substitute URLs...~%")
453 #:cache-urls cache-urls 493 store-item)
454 #:acl acl 494 (fallback)))
455 #:deduplicate? deduplicate? 495 (download-nar narinfo destination
456 #:print-build-trace? 496 #:deduplicate? deduplicate?
457 print-build-trace? 497 #:print-build-trace? print-build-trace?
458 #:fast-decompression? 498 #:fast-decompression? fast-decompression?
459 fast-decompression?))) 499 #:open-connection-for-uri
460 (download-nar narinfo destination 500 open-connection-for-uri/cached
461 #:deduplicate? deduplicate? 501 #:keep-alive? #t)))
462 #:print-build-trace? print-build-trace? 502 (else
463 #:fast-decompression? fast-decompression? 503 (fallback)))))
464 #:open-connection-for-uri
465 open-connection-for-uri/cached
466 #:keep-alive? #t))))
467 (values narinfo 504 (values narinfo
468 expected-hash 505 expected-hash
469 actual-hash))) 506 actual-hash)))
@@ -519,10 +556,17 @@ substitutes may be unavailable\n")))))
519found." 556found."
520 (assoc-ref (force options) option)))) 557 (assoc-ref (force options) option))))
521 558
559(define (assert-safe-uris uris)
560 (unless (or (%allow-unsafe-narinfo-uris?)
561 (every uri-safe? uris))
562 (leave (G_ "unsafe or invalid URI in ~S~%") uris))
563 uris)
564
522(define %default-substitute-urls 565(define %default-substitute-urls
523 (match (and=> (or (find-daemon-option "untrusted-substitute-urls") ;client 566 (match (or (and=> (find-daemon-option "untrusted-substitute-urls") ;client
524 (find-daemon-option "substitute-urls")) ;admin 567 (compose assert-safe-uris string-tokenize))
525 string-tokenize) 568 (and=> (find-daemon-option "substitute-urls") ;admin
569 string-tokenize))
526 ((urls ...) 570 ((urls ...)
527 urls) 571 urls)
528 (#f 572 (#f
@@ -558,8 +602,9 @@ is shorter than MAX elements, then it is directly returned."
558 (let* ((option (find-daemon-option "discover")) 602 (let* ((option (find-daemon-option "discover"))
559 (discover? (and option (string=? option "true")))) 603 (discover? (and option (string=? option "true"))))
560 (if discover? 604 (if discover?
561 (randomize-substitute-urls (read-substitute-urls)) 605 (randomize-substitute-urls (filter uri-safe?
562 '()))) 606 (read-substitute-urls)))
607 '())))
563 608
564(define substitute-urls 609(define substitute-urls
565 ;; List of substitute URLs. 610 ;; List of substitute URLs.
diff --git a/tests/substitute.scm b/tests/substitute.scm
index cd75d56b981..10b74db084e 100644
--- a/tests/substitute.scm
+++ b/tests/substitute.scm
@@ -206,6 +206,26 @@ a file for NARINFO."
206;; daemon. 206;; daemon.
207(%reply-file-descriptor #f) 207(%reply-file-descriptor #f)
208 208
209(test-equal "file:// URI prohibited by default"
210 "not-found\n"
211 (with-output-to-string
212 (lambda ()
213 (%allow-unsafe-substitute-uris? #f)
214 (let ((narinfo %narinfo))
215 (with-narinfo (string-append narinfo "Signature: "
216 (signature-field narinfo) "\n")
217 (call-with-temporary-directory
218 (lambda (directory)
219 (with-input-from-string (string-append
220 "substitute " (%store-prefix)
221 "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-foo "
222 directory "/shouldnotbewritten\n")
223 (lambda ()
224 (guix-substitute "--substitute"))))))))))
225
226;; Allow these for ease of testing for the rest of the tests
227(%allow-unsafe-substitute-uris? #t)
228
209 229
210(test-equal "query narinfo without signature" 230(test-equal "query narinfo without signature"
211 "" ; not substitutable 231 "" ; not substitutable