diff options
| author | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-28 05:54:40 +0000 |
|---|---|---|
| committer | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-30 16:03:57 +0000 |
| commit | 26a29f61dcf6b2d19efc4e63b3d518d8d7337343 (patch) | |
| tree | cbab288091a289ff910b168d95890b6cf5765331 | |
| parent | d9c64368ab474b084f84c8eeeb7e0947d2ca4c22 (diff) | |
QtQml: Reject file URLs that can't be represented as local files
Since qtbase's 88e3ceb05106d69746fb9d0528b4c4f8df417e9a,
QUrl::toLocalFile() returns a null string if the fully decoded path
would contain a NUL or a percent-encoded directory separator (%2F, or
%5C on Windows). The type loader passed that empty file name on to
isResource(), which asserted when trying to read its first character.
Fail the load with a proper error instead. Adapt the Loader tests that
relied on %2F being decoded into a directory separator, and fix the
url documentation example that encoded a whole path with
encodeURIComponent().
Amend the documentation for percent-encoded characters in URLs to
highlight the problem.
[ChangeLog][QtQml][Important Behavior Changes] Local file URLs whose
paths contain a percent-encoded slash (%2F), a percent-encoded NUL
(%00), or, on Windows, a percent-encoded backslash (%5C) cannot be
loaded anymore. Previously such a %2F was decoded into a directory
separator after URL resolution and normalization had already happened.
Therefore, "%2F..%2F" sequences could escape a directory that a URL
had been checked to be contained in. As specified in RFC 3986 and in
ECMA-262's URI handling functions, a percent-encoded slash represents
a literal "/" character inside a path segment, not a delimiter.
encodeURIComponent() is meant to be used on individual URL components,
and decodeURI() deliberately does not decode %2F. A literal "/" can't
be part of a file name, so such URLs don't denote any local file.
Chromium rejects such file URLs for the same reason. If you build file
URLs from paths in JavaScript, apply encodeURIComponent() to each path
segment separately, not to the whole path.
Pick-to: 6.12 6.8
Fixes: QTBUG-150898
Change-Id: I0ba5158f7ccab4f57850f4a5524dab051c6e9287
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
| -rw-r--r-- | src/qml/doc/src/qmllanguageref/typesystem/valuetypes.qdoc | 27 | ||||
| -rw-r--r-- | src/qml/qml/qqmltypeloader.cpp | 6 | ||||
| -rw-r--r-- | tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp | 25 | ||||
| -rw-r--r-- | tests/auto/quick/qquickloader/tst_qquickloader.cpp | 12 |
4 files changed, 60 insertions, 10 deletions
diff --git a/src/qml/doc/src/qmllanguageref/typesystem/valuetypes.qdoc b/src/qml/doc/src/qmllanguageref/typesystem/valuetypes.qdoc index f26a4d9870..9ff4065ea1 100644 --- a/src/qml/doc/src/qmllanguageref/typesystem/valuetypes.qdoc +++ b/src/qml/doc/src/qmllanguageref/typesystem/valuetypes.qdoc @@ -316,22 +316,35 @@ property is only invoked when the property is reassigned to a different object v The individual elements that use URLs have to resolve them themselves. \note When referring to files stored with the \l{resources.html}{Qt Resource System} - from within QML, you should use "qrc:///" instead of ":/" as QML requires URL paths. - Relative URLs resolved from within that file will use the same protocol. + from within QML, you should use "qrc:///" instead of ":/" as QML requires + URLs rather than paths. Relative URLs resolved from within that file will + use the same scheme. - Additionally, URLs may contain encoded characters using the 'percent-encoding' scheme - specified by \l {https://datatracker.ietf.org/doc/html/rfc3986}{RFC 3986}. These characters - will be preserved within properties of type \c url, to allow QML code to - construct precise URL values. + \note Additionally, URLs may contain encoded characters using the + 'percent-encoding' scheme specified by + \l {https://datatracker.ietf.org/doc/html/rfc3986}{RFC 3986}. These + characters will be preserved within properties of type \c url, to allow QML + code to construct precise URL values. For example, a local file containing a '#' character, which would normally be interpreted as the beginning of the URL 'fragment' element, can be accessed by encoding the characters of the file name: \qml - Image { source: encodeURIComponent("/tmp/test#1.png") } + Image { source: "file:///tmp/" + encodeURIComponent("test#1.png") } \endqml + However, this also holds for slashes, backslashes, and \c nul characters. + If you do \c{encodeURIComponent("/tmp/test#1.png")} you get + \c{%2Ftmp%2Ftest%231.png}. This is generally not what you want and in + particular it is \e not a local file \e path. The encoded slashes are now + part of the file \e name and resolving this URL in the context of, say, + \c{qrc:/qt/qml/MyModule/Main.qml} results in the following URL: + \c{qrc:/qt/qml/MyModule/%2Ftmp%2Ftest%231.png}. + + If you need more control over the construction of URLs, you can use a JavaScript + \l{https://developer.mozilla.org/en-US/docs/Web/API/URL}{URL} object. + This value type is provided by the QML language. \sa {QML Value Types} diff --git a/src/qml/qml/qqmltypeloader.cpp b/src/qml/qml/qqmltypeloader.cpp index 3596cc8f7d..fd42e3c5cb 100644 --- a/src/qml/qml/qqmltypeloader.cpp +++ b/src/qml/qml/qqmltypeloader.cpp @@ -303,6 +303,12 @@ void QQmlTypeLoader::loadThread(const QQmlDataBlob::Ptr &blob) if (QQmlFile::isSynchronous(blob->m_url)) { const QString fileName = QQmlFile::urlToLocalFileOrQrc(blob->m_url); + if (fileName.isEmpty()) { + blob->setError(QLatin1String("URL cannot be represented as a local file: " + "unexpected NUL or percent-encoded directory separator " + "(%2F, or %5C on Windows).")); + return; + } const bool exists = fileExists(fileName); if (!exists && !isResource(fileName) && QFileInfo::exists(fileName)) { diff --git a/tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp b/tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp index 1631088d0f..e4799a1e38 100644 --- a/tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp +++ b/tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp @@ -68,6 +68,8 @@ private slots: void removeFromCache(); void addImportPathDuringAsyncLoad(); void addImportPathFromStatusChanged(); + void urlNotRepresentableAsLocalFile_data(); + void urlNotRepresentableAsLocalFile(); private: void checkSingleton(const QString & dataDirectory); @@ -1120,6 +1122,29 @@ void tst_QQMLTypeLoader::addImportPathFromStatusChanged() QVERIFY(engine.importPathList().contains(extraPath)); } +void tst_QQMLTypeLoader::urlNotRepresentableAsLocalFile_data() +{ + QTest::addColumn<QUrl>("url"); + + QTest::newRow("encoded slash") << dataDirectoryUrl().resolved(QUrl("subdir%2FSimple.qml")); + QTest::newRow("encoded NUL at end") << dataDirectoryUrl().resolved(QUrl("Simple.qml%00")); + QTest::newRow("encoded NUL within") << dataDirectoryUrl().resolved(QUrl("Embed%00ded.qml")); +} + +void tst_QQMLTypeLoader::urlNotRepresentableAsLocalFile() +{ + QFETCH(QUrl, url); + + QQmlEngine engine; + QQmlComponent component(&engine, url); + QVERIFY(component.isError()); + QCOMPARE(component.errors().size(), 1); + QCOMPARE(component.errors().first().description(), + QLatin1String("URL cannot be represented as a local file: " + "unexpected NUL or percent-encoded directory separator " + "(%2F, or %5C on Windows).")); +} + QTEST_MAIN(tst_QQMLTypeLoader) #include "tst_qqmltypeloader.moc" diff --git a/tests/auto/quick/qquickloader/tst_qquickloader.cpp b/tests/auto/quick/qquickloader/tst_qquickloader.cpp index 73c6838a86..146ebbfdf5 100644 --- a/tests/auto/quick/qquickloader/tst_qquickloader.cpp +++ b/tests/auto/quick/qquickloader/tst_qquickloader.cpp @@ -208,9 +208,15 @@ void tst_QQuickLoader::sourceOrComponent_data() QTest::newRow("source") << "source" << "source: 'Rect120x60.qml'\n" << QUrl("Rect120x60.qml") << ""; QTest::newRow("source with subdir") << "source" << "source: 'subdir/Test.qml'\n" << QUrl("subdir/Test.qml") << ""; - QTest::newRow("source with encoded subdir literal") << "source" << "source: 'subdir%2fTest.qml'\n" << QUrl("subdir%2FTest.qml") << ""; - QTest::newRow("source with encoded subdir optimized binding") << "source" << "source: 'subdir' + '%2fTest.qml'\n" << QUrl("subdir%2FTest.qml") << ""; - QTest::newRow("source with encoded subdir binding") << "source" << "source: encodeURIComponent('subdir/Test.qml')\n" << QUrl("subdir%2FTest.qml") << ""; + + // An encoded slash is not a directory separator and can't be part of a local file name. + const QString encodedSubdirError = dataDirectoryUrl().resolved(QUrl("subdir%2FTest.qml")).toString() + + QLatin1String(": URL cannot be represented as a local file: " + "unexpected NUL or percent-encoded directory separator " + "(%2F, or %5C on Windows)."); + QTest::newRow("source with encoded subdir literal") << "source" << "source: 'subdir%2fTest.qml'\n" << QUrl("subdir%2FTest.qml") << encodedSubdirError; + QTest::newRow("source with encoded subdir optimized binding") << "source" << "source: 'subdir' + '%2fTest.qml'\n" << QUrl("subdir%2FTest.qml") << encodedSubdirError; + QTest::newRow("source with encoded subdir binding") << "source" << "source: encodeURIComponent('subdir/Test.qml')\n" << QUrl("subdir%2FTest.qml") << encodedSubdirError; QTest::newRow("sourceComponent") << "component" << "Component { id: comp; Rectangle { width: 100; height: 50 } }\n sourceComponent: comp\n" << QUrl() << ""; QTest::newRow("invalid source") << "source" << "source: 'IDontExist.qml'\n" << QUrl("IDontExist.qml") << QString(testFileUrl("IDontExist.qml").toString() + ": No such file or directory"); |
