diff options
| author | Reepca Russelstein <reepca@russelstein.xyz> | 2026-06-21 07:28:58 -0500 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2026-06-24 15:20:16 +0200 |
| commit | 247245fbef1923a3bc17a23627e9c016900b4edc (patch) | |
| tree | ac70e5c649e39ce49b6271e7f30432c1fa50bff4 | |
| parent | 69b37a8e5d166328fa3d9a7c61b7ca1dfa772552 (diff) | |
daemon: libstore: reject invalid store paths in importPath.
Previously an authorized substitute server could produce invalid store paths -
that is, paths that do denote a top-level file in the store, but that do not
obey the syntax restrictions beyond what that implies. Given that an
authorized substitute server can already potentially do a lot of damage if it
really wanted to, this isn't a major issue, but closing off this opportunity
does simplify the analysis somewhat.
* nix/libstore/local-store.cc (LocalStore::importPath): use
strict readStorePath(s) variants.
* tests/store.scm ("import path not in store, unsigned", "import path not in
store, signed", "import invalid path, unsigned", "import invalid path,
signed" test cases): new test cases. The "not in store" cases succeeded
previously, while the "invalid path" cases did not succeed prior to this
commit.
Fixes: guix/guix#9078
Change-Id: Ib81c19ec1ae0fff5b7c7268f4f7429b16a870996
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Merges: #9434
| -rw-r--r-- | nix/libstore/local-store.cc | 10 | ||||
| -rw-r--r-- | tests/store.scm | 107 |
2 files changed, 114 insertions, 3 deletions
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc index b8308b1aae5..478f2ccc44f 100644 --- a/nix/libstore/local-store.cc +++ b/nix/libstore/local-store.cc | |||
| @@ -1340,12 +1340,16 @@ Path LocalStore::importPath(bool requireSignature, Source & source) | |||
| 1340 | if (magic != EXPORT_MAGIC) | 1340 | if (magic != EXPORT_MAGIC) |
| 1341 | throw Error("normalized archive cannot be imported; wrong format"); | 1341 | throw Error("normalized archive cannot be imported; wrong format"); |
| 1342 | 1342 | ||
| 1343 | Path dstPath = readStorePath(hashAndReadSource); | 1343 | /* The path being imported must at least be syntactically valid. This |
| 1344 | * doesn't guarantee that it can be constructed by some existing method, | ||
| 1345 | * but it at least rules out paths like "/gnu/store/nix-12982-1" or | ||
| 1346 | * "/gnu/store/." or "/gnu/store/..". */ | ||
| 1347 | Path dstPath = readStorePathStrict(hashAndReadSource); | ||
| 1344 | 1348 | ||
| 1345 | PathSet references = readStorePaths<PathSet>(hashAndReadSource); | 1349 | PathSet references = readStorePathsStrict<PathSet>(hashAndReadSource); |
| 1346 | 1350 | ||
| 1347 | Path deriver = readString(hashAndReadSource); | 1351 | Path deriver = readString(hashAndReadSource); |
| 1348 | if (deriver != "") assertStorePath(deriver); | 1352 | if (deriver != "") assertStorePathStrict(deriver); |
| 1349 | 1353 | ||
| 1350 | Hash hash = hashAndReadSource.hashSink.finish().first; | 1354 | Hash hash = hashAndReadSource.hashSink.finish().first; |
| 1351 | hashAndReadSource.hashing = false; | 1355 | hashAndReadSource.hashing = false; |
diff --git a/tests/store.scm b/tests/store.scm index 2eb6b778396..a3e67ac7ea4 100644 --- a/tests/store.scm +++ b/tests/store.scm | |||
| @@ -37,7 +37,9 @@ | |||
| 37 | #:use-module (guix gexp) | 37 | #:use-module (guix gexp) |
| 38 | #:use-module (gnu packages) | 38 | #:use-module (gnu packages) |
| 39 | #:use-module (gnu packages bootstrap) | 39 | #:use-module (gnu packages bootstrap) |
| 40 | #:use-module (ice-9 binary-ports) | ||
| 40 | #:use-module (ice-9 match) | 41 | #:use-module (ice-9 match) |
| 42 | #:use-module (ice-9 rdelim) | ||
| 41 | #:use-module (ice-9 regex) | 43 | #:use-module (ice-9 regex) |
| 42 | #:use-module (rnrs bytevectors) | 44 | #:use-module (rnrs bytevectors) |
| 43 | #:use-module (rnrs io ports) | 45 | #:use-module (rnrs io ports) |
| @@ -1522,6 +1524,111 @@ System: x86_64-linux~%" | |||
| 1522 | (pk 'corrupt-imported imported) | 1524 | (pk 'corrupt-imported imported) |
| 1523 | #f))))) | 1525 | #f))))) |
| 1524 | 1526 | ||
| 1527 | |||
| 1528 | (define* (plain-dump filename contents #:key signed?) | ||
| 1529 | (let* ((contents (if (bytevector? contents) | ||
| 1530 | contents | ||
| 1531 | (string->utf8 contents))) | ||
| 1532 | (item-dump (call-with-bytevector-output-port | ||
| 1533 | (lambda (port) | ||
| 1534 | (call-with-input-bytevector | ||
| 1535 | contents | ||
| 1536 | (lambda (contents-port) | ||
| 1537 | (write-file-tree #t port | ||
| 1538 | #:file-type+size (lambda (_) | ||
| 1539 | (values 'regular | ||
| 1540 | (bytevector-length | ||
| 1541 | contents))) | ||
| 1542 | #:file-port (const contents-port)))) | ||
| 1543 | (write-int #x4558494e port) ;%export-magic | ||
| 1544 | (write-string filename port) ;store item | ||
| 1545 | (write-string-list '() port) ;references | ||
| 1546 | (write-string "" port))))) ;deriver | ||
| 1547 | (call-with-bytevector-output-port | ||
| 1548 | (lambda (port) | ||
| 1549 | (write-int 1 port) ;start | ||
| 1550 | (put-bytevector port item-dump) | ||
| 1551 | (write-int (if signed? 1 0) port) ;signed | ||
| 1552 | (when signed? | ||
| 1553 | (let* ((read-canonical-sexp | ||
| 1554 | (compose gcrypt:string->canonical-sexp read-string)) | ||
| 1555 | (public-key (call-with-input-file %public-key-file | ||
| 1556 | read-canonical-sexp)) | ||
| 1557 | (private-key (call-with-input-file %private-key-file | ||
| 1558 | read-canonical-sexp))) | ||
| 1559 | (write-string (gcrypt:canonical-sexp->string | ||
| 1560 | (signature-sexp | ||
| 1561 | (gcrypt:bytevector->hash-data | ||
| 1562 | (gcrypt:sha256 item-dump) | ||
| 1563 | #:key-type (gcrypt:key-type public-key)) | ||
| 1564 | private-key | ||
| 1565 | public-key)) | ||
| 1566 | port))) | ||
| 1567 | (write-int 0 port))))) | ||
| 1568 | |||
| 1569 | (test-assert "import path not in store, unsigned" | ||
| 1570 | ;; error should be produced before the signature is even considered | ||
| 1571 | (call-with-input-bytevector | ||
| 1572 | (plain-dump (string-append (%store-prefix) "/../../notinstore") | ||
| 1573 | (random-text)) | ||
| 1574 | (lambda (port) | ||
| 1575 | (guard (c ((store-protocol-error? c) | ||
| 1576 | (pk 'c c) | ||
| 1577 | (and (not (zero? (store-protocol-error-status c))) | ||
| 1578 | (let ((message (store-protocol-error-message c))) | ||
| 1579 | (pk 'error-message message) | ||
| 1580 | (or (string-contains message "is not a valid store path") | ||
| 1581 | (string-contains message "is not in the store")))))) | ||
| 1582 | (import-paths %store port) | ||
| 1583 | #f)))) | ||
| 1584 | |||
| 1585 | (test-assert "import path not in store, signed" | ||
| 1586 | (call-with-input-bytevector | ||
| 1587 | (plain-dump (string-append (%store-prefix) "/../../notinstore") | ||
| 1588 | (random-text)) | ||
| 1589 | (lambda (port) | ||
| 1590 | (guard (c ((store-protocol-error? c) | ||
| 1591 | (pk 'c c) | ||
| 1592 | (and (not (zero? (store-protocol-error-status c))) | ||
| 1593 | (let ((message (store-protocol-error-message c))) | ||
| 1594 | (pk 'error-message message) | ||
| 1595 | (or (string-contains message "is not a valid store path") | ||
| 1596 | (string-contains message "is not in the store")))))) | ||
| 1597 | (import-paths %store port) | ||
| 1598 | #f)))) | ||
| 1599 | |||
| 1600 | (test-assert "import invalid path, unsigned" | ||
| 1601 | ;; error should be produced before the signature is even considered | ||
| 1602 | (call-with-input-bytevector | ||
| 1603 | (plain-dump (string-append (%store-prefix) | ||
| 1604 | "/!@#$%^&*()_+=-[]{}\\|';:,<>.") | ||
| 1605 | (random-text)) | ||
| 1606 | (lambda (port) | ||
| 1607 | (guard (c ((store-protocol-error? c) | ||
| 1608 | (pk 'c c) | ||
| 1609 | (and (not (zero? (store-protocol-error-status c))) | ||
| 1610 | (let ((message (store-protocol-error-message c))) | ||
| 1611 | (pk 'error-message message) | ||
| 1612 | (string-contains message "is not a valid store path"))))) | ||
| 1613 | (import-paths %store port) | ||
| 1614 | #f)))) | ||
| 1615 | |||
| 1616 | (test-assert "import invalid path, signed" | ||
| 1617 | (call-with-input-bytevector | ||
| 1618 | (plain-dump (string-append (%store-prefix) | ||
| 1619 | "/!@#$%^&*()_+=-[]{}\\|';:,<>.") | ||
| 1620 | (random-text) | ||
| 1621 | #:signed? #t) | ||
| 1622 | (lambda (port) | ||
| 1623 | (guard (c ((store-protocol-error? c) | ||
| 1624 | (pk 'c c) | ||
| 1625 | (and (not (zero? (store-protocol-error-status c))) | ||
| 1626 | (let ((message (store-protocol-error-message c))) | ||
| 1627 | (pk 'error-message message) | ||
| 1628 | (string-contains message "is not a valid store path"))))) | ||
| 1629 | (import-paths %store port) | ||
| 1630 | #f)))) | ||
| 1631 | |||
| 1525 | (test-assert "verify-store" | 1632 | (test-assert "verify-store" |
| 1526 | (let* ((text (random-text)) | 1633 | (let* ((text (random-text)) |
| 1527 | (file1 (add-text-to-store %store "foo" text)) | 1634 | (file1 (add-text-to-store %store "foo" text)) |
