-
Notifications
You must be signed in to change notification settings - Fork 965
fix(UI): validate E2EE file names #10238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
68c082f
91d3ec0
047b9ec
09e3864
b05f17b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
@@ -9,6 +9,7 @@ | |
| #include "clientsideencryption.h" | ||
| #include "clientsideencryptionjobs.h" | ||
| #include <common/checksums.h> | ||
| #include <QDir> | ||
| #include <QJsonArray> | ||
| #include <QJsonDocument> | ||
| #include <QSslCertificate> | ||
|
|
@@ -49,6 +50,22 @@ | |
| } | ||
| } | ||
|
|
||
| bool FolderMetadata::isOriginalFilenameValid(const QString &originalFilename) | ||
| { | ||
| if (originalFilename.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| if (originalFilename.contains(QLatin1Char('/')) | ||
| || originalFilename.contains(QLatin1Char('\\')) | ||
| || originalFilename.contains(QChar(0))) { | ||
| return false; | ||
| } | ||
|
|
||
| const auto slashPrefixedName = QStringLiteral("/") + originalFilename; | ||
| return QDir::cleanPath(slashPrefixedName) == slashPrefixedName; | ||
| } | ||
|
|
||
| bool FolderMetadata::EncryptedFile::isDirectory() const | ||
| { | ||
| return mimetype.isEmpty() || mimetype == QByteArrayLiteral("inode/directory") || mimetype == QByteArrayLiteral("httpd/unix-directory"); | ||
|
|
@@ -119,6 +136,11 @@ | |
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| return; | ||
| } | ||
| if (_existingMetadataVersion < MetadataVersion::Version2_0 && !_initialSignature.isEmpty()) { | ||
| qCWarning(lcCseMetadata()) << "Could not setup legacy metadata with a V2 signature."; | ||
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| return; | ||
| } | ||
| if (_existingMetadataVersion < MetadataVersion::Version2_0) { | ||
| setupExistingMetadataLegacy(metadata); | ||
| return; | ||
|
|
@@ -254,12 +276,20 @@ | |
|
|
||
| for (auto it = folders.constBegin(); it != folders.constEnd(); ++it) { | ||
| const auto folderName = it.value().toString(); | ||
| if (!folderName.isEmpty()) { | ||
| EncryptedFile file; | ||
| file.encryptedFilename = it.key(); | ||
| file.originalFilename = folderName; | ||
| _files.push_back(file); | ||
| if (folderName.isEmpty()) { | ||
| continue; | ||
| } | ||
|
|
||
| if (!isOriginalFilenameValid(folderName)) { | ||
| qCWarning(lcCseMetadata()) << "skipping encrypted folder" << it.key() << "metadata has an invalid file name"; | ||
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| continue; | ||
| } | ||
|
|
||
| EncryptedFile file; | ||
| file.encryptedFilename = it.key(); | ||
| file.originalFilename = folderName; | ||
| _files.push_back(file); | ||
| } | ||
| _isMetadataValid = true; | ||
| } | ||
|
|
@@ -344,12 +374,19 @@ | |
|
|
||
| const auto decryptedFileObj = decryptedFileDoc.object(); | ||
|
|
||
| if (decryptedFileObj["filename"].toString().isEmpty()) { | ||
| const auto originalFilename = decryptedFileObj["filename"].toString(); | ||
| if (originalFilename.isEmpty()) { | ||
| qCWarning(lcCseMetadata) << "decrypted metadata" << decryptedFileDoc.toJson(QJsonDocument::Compact) << "skipping encrypted file" << file.encryptedFilename << "metadata has an empty file name"; | ||
| continue; | ||
| } | ||
|
|
||
| file.originalFilename = decryptedFileObj["filename"].toString(); | ||
| if (!isOriginalFilenameValid(originalFilename)) { | ||
| qCWarning(lcCseMetadata) << "skipping encrypted file" << file.encryptedFilename << "metadata has an invalid file name"; | ||
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| continue; | ||
| } | ||
|
|
||
| file.originalFilename = originalFilename; | ||
| file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit()); | ||
| file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit(); | ||
|
|
||
|
|
@@ -503,10 +540,17 @@ | |
| FolderMetadata::EncryptedFile FolderMetadata::parseEncryptedFileFromJson(const QString &encryptedFilename, const QJsonValue &fileJSON) const | ||
| { | ||
| const auto fileObj = fileJSON.toObject(); | ||
| if (fileObj["filename"].toString().isEmpty()) { | ||
| const auto originalFilename = fileObj["filename"].toString(); | ||
| if (originalFilename.isEmpty()) { | ||
| qCWarning(lcCseMetadata()) << "skipping encrypted file" << encryptedFilename << "metadata has an empty file name"; | ||
| return {}; | ||
| } | ||
|
|
||
| if (!isOriginalFilenameValid(originalFilename)) { | ||
| qCWarning(lcCseMetadata()) << "skipping encrypted file" << encryptedFilename << "metadata has an invalid file name"; | ||
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| return {}; | ||
| } | ||
|
|
||
| EncryptedFile file; | ||
| file.encryptedFilename = encryptedFilename; | ||
|
|
@@ -516,7 +560,7 @@ | |
| nonce = QByteArray::fromBase64(fileObj[nonceKey].toString().toLocal8Bit()); | ||
| } | ||
| file.initializationVector = nonce; | ||
| file.originalFilename = fileObj["filename"].toString(); | ||
| file.originalFilename = originalFilename; | ||
| file.encryptionKey = QByteArray::fromBase64(fileObj["key"].toString().toLocal8Bit()); | ||
| file.mimetype = fileObj["mimetype"].toString().toLocal8Bit(); | ||
|
|
||
|
|
@@ -530,6 +574,11 @@ | |
|
|
||
| QJsonObject FolderMetadata::convertFileToJsonObject(const EncryptedFile *encryptedFile) const | ||
| { | ||
| if (!encryptedFile || !isOriginalFilenameValid(encryptedFile->originalFilename)) { | ||
| qCWarning(lcCseMetadata()) << "Metadata generation failed. Invalid original file name."; | ||
| return {}; | ||
| } | ||
|
|
||
| QJsonObject file; | ||
| file.insert("key", QString(encryptedFile->encryptionKey.toBase64())); | ||
| file.insert("filename", encryptedFile->originalFilename); | ||
|
|
@@ -723,6 +772,12 @@ | |
|
|
||
| QJsonObject files; | ||
| for (auto it = _files.constBegin(), end = _files.constEnd(); it != end; ++it) { | ||
| if (!isOriginalFilenameValid(it->originalFilename)) { | ||
| qCWarning(lcCseMetadata) << "Metadata generation failed. Invalid original file name for encrypted file" << it->encryptedFilename; | ||
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| return {}; | ||
| } | ||
|
|
||
| QJsonObject encrypted; | ||
| encrypted.insert("key", QString(it->encryptionKey.toBase64())); | ||
| encrypted.insert("filename", it->originalFilename); | ||
|
|
@@ -921,6 +976,12 @@ | |
| return; | ||
| } | ||
|
|
||
| if (!isOriginalFilenameValid(f.originalFilename)) { | ||
| qCWarning(lcCseMetadata()) << "Could not add encrypted file with invalid original file name."; | ||
| _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); | ||
| return; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a POSIX client syncs an E2EE file whose basename contains a backslash, local discovery can still allow it unless Windows compatibility or server-forbidden characters are active, but this new guard only returns from Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| for (int i = 0; i < _files.size(); ++i) { | ||
| if (_files.at(i).originalFilename == f.originalFilename) { | ||
| _files.removeAt(i); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an existing encrypted folder's metadata contains an invalid original name, this branch returns an empty
EncryptedFile;setupExistingMetadata()then just skips appending it and still marks the metadata valid. In that scenario the corresponding server entry remains in the PROPFIND results without ane2eMangledName/encrypted flag, so discovery can treat the encrypted blob itself as a normal plaintext child under its mangled server name instead of failing the sync. Please make parsing fail the metadata setup, or otherwise filter that remote entry, rather than silently dropping the mapping.Useful? React with 👍 / 👎.