aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2026-09-28 05:54:40 +0000
committerUlf Hermann <ulf.hermann@qt.io>2026-09-30 16:03:57 +0000
commit26a29f61dcf6b2d19efc4e63b3d518d8d7337343 (patch)
treecbab288091a289ff910b168d95890b6cf5765331
parentd9c64368ab474b084f84c8eeeb7e0947d2ca4c22 (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.qdoc27
-rw-r--r--src/qml/qml/qqmltypeloader.cpp6
-rw-r--r--tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp25
-rw-r--r--tests/auto/quick/qquickloader/tst_qquickloader.cpp12
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");