Skip to content

Commit 47f427a

Browse files
committed
Remove validation of URLs passed to FileTransferRequest verbatim
CURL is not very strict about validation of URLs passed to it. We should reflect this in our handling of URLs that we get from the user in <nix/fetchurl.nix> or builtins.fetchurl. ValidURL was an attempt to rectify this, but it turned out to be too strict. The only good way to resolve this is to pass (in some cases) the user-provided string verbatim to CURL. Other usages in libfetchers still benefit from using structured ParsedURL and validation though. nix store prefetch-file --name foo 'https://cdn.skypack.dev/big.js@^5.2.2' error: 'https://cdn.skypack.dev/big.js@^5.2.2' is not a valid URL: leftover
1 parent 0f85ef3 commit 47f427a

File tree

7 files changed

+44
-37
lines changed

7 files changed

+44
-37
lines changed

src/libfetchers/tarball.cc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ DownloadFileResult downloadFile(
4242
if (cached && !cached->expired)
4343
return useCached();
4444

45-
FileTransferRequest request(ValidURL{url});
45+
FileTransferRequest request(VerbatimURL{url});
4646
request.headers = headers;
4747
if (cached)
4848
request.expectedETag = getStrAttr(cached->value, "etag");
@@ -107,13 +107,13 @@ DownloadFileResult downloadFile(
107107
static DownloadTarballResult downloadTarball_(
108108
const Settings & settings, const std::string & urlS, const Headers & headers, const std::string & displayPrefix)
109109
{
110-
ValidURL url = urlS;
110+
ParsedURL url = parseURL(urlS);
111111

112112
// Some friendly error messages for common mistakes.
113113
// Namely lets catch when the url is a local file path, but
114114
// it is not in fact a tarball.
115-
if (url.scheme() == "file") {
116-
std::filesystem::path localPath = renderUrlPathEnsureLegal(url.path());
115+
if (url.scheme == "file") {
116+
std::filesystem::path localPath = renderUrlPathEnsureLegal(url.path);
117117
if (!exists(localPath)) {
118118
throw Error("tarball '%s' does not exist.", localPath);
119119
}
@@ -164,7 +164,7 @@ static DownloadTarballResult downloadTarball_(
164164

165165
/* Note: if the download is cached, `importTarball()` will receive
166166
no data, which causes it to import an empty tarball. */
167-
auto archive = !url.path().empty() && hasSuffix(toLower(url.path().back()), ".zip") ? ({
167+
auto archive = !url.path.empty() && hasSuffix(toLower(url.path.back()), ".zip") ? ({
168168
/* In streaming mode, libarchive doesn't handle
169169
symlinks in zip files correctly (#10649). So write
170170
the entire file to disk so libarchive can access it
@@ -178,7 +178,7 @@ static DownloadTarballResult downloadTarball_(
178178
}
179179
TarArchive{path};
180180
})
181-
: TarArchive{*source};
181+
: TarArchive{*source};
182182
auto tarballCache = getTarballCache();
183183
auto parseSink = tarballCache->getFileSystemObjectSink();
184184
auto lastModified = unpackTarfileToSink(archive, *parseSink);

src/libstore/builtins/fetchurl.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ static void builtinFetchurl(const BuiltinBuilderContext & ctx)
3737

3838
auto fetch = [&](const std::string & url) {
3939
auto source = sinkToSource([&](Sink & sink) {
40-
FileTransferRequest request(ValidURL{url});
40+
FileTransferRequest request(VerbatimURL{url});
4141
request.decompress = false;
4242

4343
auto decompressor = makeDecompressionSink(unpack && hasSuffix(mainUrl, ".xz") ? "xz" : "none", sink);

src/libstore/include/nix/store/filetransfer.hh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ struct UsernameAuth
9595

9696
struct FileTransferRequest
9797
{
98-
ValidURL uri;
98+
VerbatimURL uri;
9999
Headers headers;
100100
std::string expectedETag;
101101
bool verifyTLS = true;
@@ -121,7 +121,7 @@ struct FileTransferRequest
121121
std::optional<std::string> preResolvedAwsSessionToken;
122122
#endif
123123

124-
FileTransferRequest(ValidURL uri)
124+
FileTransferRequest(VerbatimURL uri)
125125
: uri(std::move(uri))
126126
, parentAct(getCurActivity())
127127
{

src/libutil/include/nix/util/url.hh

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
#include "nix/util/error.hh"
88
#include "nix/util/canon-path.hh"
9+
#include "nix/util/split.hh"
10+
#include "nix/util/util.hh"
11+
#include "nix/util/variant-wrapper.hh"
912

1013
namespace nix {
1114

@@ -342,8 +345,7 @@ ParsedURL fixGitURL(const std::string & url);
342345
bool isValidSchemeName(std::string_view scheme);
343346

344347
/**
345-
* Either a ParsedURL or a verbatim string, but the string must be a valid
346-
* ParsedURL. This is necessary because in certain cases URI must be passed
348+
* Either a ParsedURL or a verbatim string. This is necessary because in certain cases URI must be passed
347349
* verbatim (e.g. in builtin fetchers), since those are specified by the user.
348350
* In those cases normalizations performed by the ParsedURL might be surprising
349351
* and undesirable, since Nix must be a universal client that has to work with
@@ -354,23 +356,23 @@ bool isValidSchemeName(std::string_view scheme);
354356
*
355357
* Though we perform parsing and validation for internal needs.
356358
*/
357-
struct ValidURL : private ParsedURL
359+
struct VerbatimURL
358360
{
359-
std::optional<std::string> encoded;
361+
using Raw = std::variant<std::string, ParsedURL>;
362+
Raw raw;
360363

361-
ValidURL(std::string str)
362-
: ParsedURL(parseURL(str, /*lenient=*/false))
363-
, encoded(std::move(str))
364+
VerbatimURL(std::string_view s)
365+
: raw(std::string{s})
364366
{
365367
}
366368

367-
ValidURL(std::string_view str)
368-
: ValidURL(std::string{str})
369+
VerbatimURL(std::string s)
370+
: raw(std::move(s))
369371
{
370372
}
371373

372-
ValidURL(ParsedURL parsed)
373-
: ParsedURL{std::move(parsed)}
374+
VerbatimURL(ParsedURL url)
375+
: raw(std::move(url))
374376
{
375377
}
376378

@@ -379,25 +381,35 @@ struct ValidURL : private ParsedURL
379381
*/
380382
std::string to_string() const
381383
{
382-
return encoded.or_else([&]() -> std::optional<std::string> { return ParsedURL::to_string(); }).value();
384+
return std::visit(
385+
overloaded{
386+
[](const std::string & str) { return str; }, [](const ParsedURL & url) { return url.to_string(); }},
387+
raw);
383388
}
384389

385-
const ParsedURL & parsed() const &
390+
const ParsedURL parsed() const
386391
{
387-
return *this;
392+
return std::visit(
393+
overloaded{
394+
[](const std::string & str) { return parseURL(str); }, [](const ParsedURL & url) { return url; }},
395+
raw);
388396
}
389397

390398
std::string_view scheme() const &
391399
{
392-
return ParsedURL::scheme;
393-
}
394-
395-
const auto & path() const &
396-
{
397-
return ParsedURL::path;
400+
return std::visit(
401+
overloaded{
402+
[](std::string_view str) {
403+
auto scheme = splitPrefixTo(str, ':');
404+
if (!scheme)
405+
throw BadURL("URL '%s' doesn't have a scheme", str);
406+
return *scheme;
407+
},
408+
[](const ParsedURL & url) -> std::string_view { return url.scheme; }},
409+
raw);
398410
}
399411
};
400412

401-
std::ostream & operator<<(std::ostream & os, const ValidURL & url);
413+
std::ostream & operator<<(std::ostream & os, const VerbatimURL & url);
402414

403415
} // namespace nix

src/libutil/url.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ bool isValidSchemeName(std::string_view s)
434434
return std::regex_match(s.begin(), s.end(), regex, std::regex_constants::match_default);
435435
}
436436

437-
std::ostream & operator<<(std::ostream & os, const ValidURL & url)
437+
std::ostream & operator<<(std::ostream & os, const VerbatimURL & url)
438438
{
439439
os << url.to_string();
440440
return os;

src/nix/prefetch.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ std::tuple<StorePath, Hash> prefetchFile(
105105

106106
FdSink sink(fd.get());
107107

108-
FileTransferRequest req(ValidURL{url});
108+
FileTransferRequest req(VerbatimURL{url});
109109
req.decompress = false;
110110
getFileTransfer()->download(std::move(req), sink);
111111
}

tests/functional/fetchurl.sh

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,3 @@ requireDaemonNewerThan "2.20"
8888
expected=100
8989
if [[ -v NIX_DAEMON_PACKAGE ]]; then expected=1; fi # work around the daemon not returning a 100 status correctly
9090
expectStderr $expected nix-build --expr '{ url }: builtins.derivation { name = "nix-cache-info"; system = "x86_64-linux"; builder = "builtin:fetchurl"; inherit url; outputHashMode = "flat"; }' --argstr url "file://$narxz" 2>&1 | grep 'must be a fixed-output or impure derivation'
91-
92-
requireDaemonNewerThan "2.32.0pre20250831"
93-
94-
expect 1 nix-build --expr 'import <nix/fetchurl.nix>' --argstr name 'name' --argstr url "file://authority.not.allowed/fetchurl.sh?a=1&a=2" --no-out-link |&
95-
grepQuiet "error: file:// URL 'file://authority.not.allowed/fetchurl.sh?a=1&a=2' has unexpected authority 'authority.not.allowed'"

0 commit comments

Comments
 (0)