diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index aaab24a38..042d777a6 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -570,6 +570,32 @@ bool plainTables(sqlite3 *db) return ok && run(QStringLiteral("VACUUM")); } + //What SQLite advises for a database read from a file + //(https://www.sqlite.org/security.html): nothing in its schema runs, no + //trigger or view either, the defensive mode, no value longer than any + //project holds, and the size of each cell checked as its page is read. + //The database @p schema of @p db ("main", or an attached one) is then + //checked whole, so that damage is said as such before anything reads + //it, instead of showing as a missing row or not at all. +bool readAsUntrusted(Database &db, const QString &schema, QString *error) +{ + sqlite3_db_config(db.m_db, SQLITE_DBCONFIG_DEFENSIVE, 1, nullptr); + sqlite3_db_config(db.m_db, SQLITE_DBCONFIG_ENABLE_TRIGGER, 0, nullptr); +#ifdef SQLITE_DBCONFIG_ENABLE_VIEW + sqlite3_db_config(db.m_db, SQLITE_DBCONFIG_ENABLE_VIEW, 0, nullptr); +#endif + db.exec(QStringLiteral("PRAGMA trusted_schema = OFF")); + db.exec(QStringLiteral("PRAGMA cell_size_check = ON")); + sqlite3_limit(db.m_db, SQLITE_LIMIT_LENGTH, 256 * 1024 * 1024); + Statement check(db, QStringLiteral("PRAGMA %1.quick_check(1)").arg(schema)); + const QString result = check.next() ? check.value(0).toString() : db.error(); + if (result != QLatin1String("ok")) { + setError(error, QStringLiteral("the project database is damaged: %1").arg(result)); + return false; + } + return true; +} + //@p database opened in @p db, its tables checked to be plain tables and //its format one this reads: a file is untrusted bool openDatabase(Database &db, const QByteArray &database, QString *error) @@ -587,9 +613,10 @@ bool openDatabase(Database &db, const QByteArray &database, QString *error) setError(error, QStringLiteral("the project database cannot be read")); return false; } - //A file is untrusted: nothing in its schema may run, and every name - //read must be a plain table. - db.exec(QStringLiteral("PRAGMA trusted_schema = OFF")); + //A file is untrusted (readAsUntrusted()), and every name read must + //be a plain table. + if (!readAsUntrusted(db, QStringLiteral("main"), error)) + return false; for (const QString &table : DocTables) { Statement kind(db, QStringLiteral("SELECT type FROM sqlite_master WHERE name = ?")); kind.bind({table}); @@ -902,7 +929,8 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB setError(error, QStringLiteral("cannot read the database the kept folios come from")); return {}; } - db.exec(QStringLiteral("PRAGMA trusted_schema = OFF")); + if (!readAsUntrusted(db, QStringLiteral("old"), error)) + return {}; for (const QString &table : DocTables) { Statement kind(db, QStringLiteral("SELECT type FROM old.sqlite_master WHERE name = ?")); kind.bind({table}); diff --git a/tests/qttest/tst_qetcontainer.cpp b/tests/qttest/tst_qetcontainer.cpp index 4c1c020dd..d1e574e31 100644 --- a/tests/qttest/tst_qetcontainer.cpp +++ b/tests/qttest/tst_qetcontainer.cpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -533,6 +534,38 @@ private slots: QVERIFY(!error.isEmpty()); } + // A page of the database overwritten, as a bad disk or a broken copy + // would: refused, and said to be damaged (quick_check on opening), + // whichever table the page held + void damagedDatabaseRefused_data() + { + QTest::addColumn("where"); + QTest::newRow("second page") << 0.0; + QTest::newRow("middle page") << 0.5; + QTest::newRow("last page") << 1.0; + } + void damagedDatabaseRefused() + { + QFETCH(double, where); + QDomDocument original; + QList entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original); + bool damaged = false; + for (QetZip::Entry &entry : entries) { + if (entry.name != QLatin1String("project.sqlite")) continue; + const int page_size = qFromBigEndian(entry.data.constData() + 16); + const int pages = int(entry.data.size() / page_size); + QVERIFY(pages > 3); + const int page = 1 + int(where * (pages - 2)); + entry.data.replace(page * page_size, page_size, QByteArray(page_size, char(0xff))); + damaged = true; + } + QVERIFY(damaged); + QDomDocument back; + QString error; + QVERIFY(!QetContainer::join(entries, &back, &error)); + QVERIFY2(error.contains(QStringLiteral("damaged")), qPrintable(error)); + } + // project.sqlite changed by @p sql in @p entries static void alterDatabase(QList &entries, const char *sql) {