summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2026-06-24 11:34:05 +0200
committerLudovic Courtès <ludo@gnu.org>2026-07-02 19:42:48 +0200
commit2ef8ed9f0df53bddf14bdecc2ea48c2d233213cc (patch)
tree6f09ca584a6ddaf7928ab0d5ca5403fd9499f14d
parent48cdb840ac38f32493e76e2c81570a1dd3c50f1f (diff)
substitutes: Ignore narinfos that don’t match the request [security fix].
Previously, by serving a valid, signed, and authorized narinfo that does not match what the client asked for, an attacker could cause users to get the wrong substitute. * guix/substitutes.scm (fetch-narinfos)[handle-narinfo-response]: Check whether the result of ‘read-narinfo’ matches ‘request’ and ignore it if it doesn’t. [do-fetch]: Likewise with ‘narinfo-from-file’. * tests/substitute.scm ("query narinfo that returns different store path") ("substitute, narinfo does not match requested store item"): New tests. Reported-by: Reepca Russelstein <reepca@russelstein.xyz> Signed-off-by: Ludovic Courtès <ludo@gnu.org> Merges: #9665
-rw-r--r--guix/substitutes.scm36
-rw-r--r--tests/substitute.scm35
2 files changed, 57 insertions, 14 deletions
diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index b2a79c2a48d..289a068dbf3 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2013-2021, 2023-2026 Ludovic Courtès <ludo@gnu.org>
3;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org> 3;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
4;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com> 4;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
5;;; Copyright © 2020 Christopher Baines <mail@cbaines.net> 5;;; Copyright © 2020 Christopher Baines <mail@cbaines.net>
@@ -230,7 +230,11 @@ it contains invalid store file names--and return the narinfo otherwise."
230 (dump-port port (%make-void-port "w"))) 230 (dump-port port (%make-void-port "w")))
231 231
232 (define (handle-narinfo-response request response port result) 232 (define (handle-narinfo-response request response port result)
233 (let* ((code (response-code response)) 233 (let* ((hash-part (basename
234 (string-drop-right (uri-path (request-uri request))
235 (string-length ".narinfo"))))
236 (path (hash-part->path hash-part))
237 (code (response-code response))
234 (len (response-content-length response)) 238 (len (response-content-length response))
235 (cache (response-cache-control response)) 239 (cache (response-cache-control response))
236 (ttl (and cache (assoc-ref cache 'max-age)))) 240 (ttl (and cache (assoc-ref cache 'max-age))))
@@ -240,21 +244,18 @@ it contains invalid store file names--and return the narinfo otherwise."
240 ;; belong to the next response. 244 ;; belong to the next response.
241 (if (= code 200) ; hit 245 (if (= code 200) ; hit
242 (let ((narinfo (read-narinfo port url #:size len))) 246 (let ((narinfo (read-narinfo port url #:size len)))
243 (if narinfo 247 (if (and narinfo path
248 ;; Make sure we got what we asked for.
249 (string=? (narinfo-path narinfo) path))
244 (begin 250 (begin
245 (cache-narinfo! url (narinfo-path narinfo) narinfo ttl) 251 (cache-narinfo! url (narinfo-path narinfo) narinfo ttl)
246 (cons narinfo result)) 252 (cons narinfo result))
247 result)) 253 result))
248 (let* ((path (uri-path (request-uri request))) 254 ;; Log the failing queries and indicate if it failed because the
249 (hash-part (basename 255 ;; narinfo is being baked.
250 (string-drop-right path 8)))) ;drop ".narinfo" 256 (let ((baking? (assoc-ref (response-headers response) 'x-baking)))
251 ;; Log the failing queries and indicate if it failed because the 257 (debug "could not fetch ~a~a ~a~a~%"
252 ;; narinfo is being baked. 258 url path code (if baking? " (baking)" ""))
253 (let ((baking?
254 (assoc-ref (response-headers response) 'x-baking)))
255 (debug "could not fetch ~a~a ~a~a~%"
256 url path code
257 (if baking? " (baking)" "")))
258 (if len 259 (if len
259 (get-bytevector-n port len) 260 (get-bytevector-n port len)
260 (read-to-eof port)) 261 (read-to-eof port))
@@ -288,7 +289,14 @@ it contains invalid store file names--and return the narinfo otherwise."
288 (files (map (compose (cut string-append base <> ".narinfo") 289 (files (map (compose (cut string-append base <> ".narinfo")
289 store-path-hash-part) 290 store-path-hash-part)
290 paths))) 291 paths)))
291 (filter-map (cut narinfo-from-file <> url) files))) 292 (filter-map (lambda (file path)
293 (let ((narinfo (narinfo-from-file file url)))
294 (and narinfo
295 ;; Make sure we got what we asked for.
296 (string=? (narinfo-path narinfo) path)
297 narinfo)))
298 files
299 paths)))
292 (else 300 (else
293 (leave (G_ "~s: unsupported server URI scheme~%") 301 (leave (G_ "~s: unsupported server URI scheme~%")
294 (if uri (uri-scheme uri) url))))) 302 (if uri (uri-scheme uri) url)))))
diff --git a/tests/substitute.scm b/tests/substitute.scm
index 8f32bfa7281..b749561347a 100644
--- a/tests/substitute.scm
+++ b/tests/substitute.scm
@@ -357,6 +357,29 @@ Deriver: foo.drv")
357 (lambda () 357 (lambda ()
358 (guix-substitute "--query"))))))))) 358 (guix-substitute "--query")))))))))
359 359
360(test-equal "query narinfo that returns different store path"
361 ;; The narinfo is valid and authorized but its 'StorePath' field points to a
362 ;; different store item.
363 ""
364
365 (let ((prefix (string-append "StorePath: " (%store-prefix)
366 "/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-BAR
367NarHash: sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
368References: bar baz\n")))
369 (with-narinfo (string-append prefix
370 "Signature: " (signature-field prefix) "
371URL: example.nar
372Compression: none
373NarSize: 42
374Deriver: foo.drv")
375 (string-trim-both
376 (with-output-to-string
377 (lambda ()
378 (with-input-from-string (string-append "have " (%store-prefix)
379 "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-foo")
380 (lambda ()
381 (guix-substitute "--query")))))))))
382
360(test-equal "query narinfo signed with authorized key" 383(test-equal "query narinfo signed with authorized key"
361 (string-append (%store-prefix) "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-foo") 384 (string-append (%store-prefix) "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-foo")
362 385
@@ -893,6 +916,18 @@ System: mips64el-linux\n")))
893 (lambda () 916 (lambda ()
894 (false-if-exception (delete-file "substitute-retrieved"))))))) 917 (false-if-exception (delete-file "substitute-retrieved")))))))
895 918
919(test-quit "substitute, narinfo does not match requested store item"
920 "no valid substitute"
921 (with-http-server `((200 ,(string-append %narinfo "Signature: "
922 (signature-field %narinfo))))
923 (parameterize ((substitute-urls (list (%local-url))))
924 ;; Narinfo is valid, signed, and authorized but requested item is
925 ;; /bbb…-bar and narinfo is for /aaa…-foo.
926 (request-substitution (string-append (%store-prefix)
927 "/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-bar")
928 "substitute-retrieved")
929 #f)))
930
896(test-end "substitute") 931(test-end "substitute")
897 932
898;;; Local Variables: 933;;; Local Variables: