(core) forbid use of sqlite ATTACH except during VACUUM

Summary:
This calls sqlite3_limit(SQLITE_LIMIT_ATTACHED, 0) so that
if ever an `ATTACH` were snuck into an SQL query, it would be denied.
The limit needs to be waived when calling VACUUM since the implementation
of VACUUM uses ATTACH.

Test Plan: added test; existing tests should pass

Reviewers: alexmojaki

Reviewed By: alexmojaki

Subscribers: alexmojaki

Differential Revision: https://phab.getgrist.com/D3316
This commit is contained in:
Paul Fitzpatrick 2022-03-11 15:24:26 -05:00
parent 3a8e7032bc
commit b2715ae9ef
4 changed files with 27 additions and 9 deletions

View File

@ -193,6 +193,6 @@ async function removeData(filename: string) {
} }
const history = new ActionHistoryImpl(db); const history = new ActionHistoryImpl(db);
await history.deleteActions(1); await history.deleteActions(1);
await db.run('VACUUM'); await db.vacuum();
await db.close(); await db.close();
} }

View File

@ -202,7 +202,7 @@ export class SQLiteDB {
let _db: sqlite3.Database; let _db: sqlite3.Database;
await fromCallback(cb => { _db = new sqlite3.Database(dbPath, sqliteMode, cb); }); await fromCallback(cb => { _db = new sqlite3.Database(dbPath, sqliteMode, cb); });
limitAttach(_db!, 0); // Outside of VACUUM, we don't allow ATTACH.
if (SQLiteDB._addOpens(dbPath, 1) > 1) { if (SQLiteDB._addOpens(dbPath, 1) > 1) {
log.warn("SQLiteDB[%s] avoid opening same DB more than once", dbPath); log.warn("SQLiteDB[%s] avoid opening same DB more than once", dbPath);
} }
@ -319,12 +319,21 @@ export class SQLiteDB {
this._needVacuum = true; this._needVacuum = true;
return false; return false;
} }
await this.exec("VACUUM"); await this.vacuum();
log.info("SQLiteDB[%s]: DB VACUUMed", this._dbPath); log.info("SQLiteDB[%s]: DB VACUUMed", this._dbPath);
this._needVacuum = false; this._needVacuum = false;
return true; return true;
} }
public async vacuum(): Promise<void> {
limitAttach(this._db, 1); // VACUUM implementation uses ATTACH.
try {
await this.exec("VACUUM");
} finally {
limitAttach(this._db, 0); // Outside of VACUUM, we don't allow ATTACH.
}
}
/** /**
* Run each of the statements in turn. Each statement is either a string, or an array of arguments * Run each of the statements in turn. Each statement is either a string, or an array of arguments
* to db.run, e.g. [sqlString, [params...]]. * to db.run, e.g. [sqlString, [params...]].
@ -480,7 +489,7 @@ export class SQLiteDB {
}); });
success = true; success = true;
// After a migration, reduce the sqlite file size. This must be run outside a transaction. // After a migration, reduce the sqlite file size. This must be run outside a transaction.
await this.run("VACUUM"); await this.vacuum();
log.info("SQLiteDB[%s]: DB backed up to %s, migrated to %s", log.info("SQLiteDB[%s]: DB backed up to %s, migrated to %s",
this._dbPath, backupPath, targetVer); this._dbPath, backupPath, targetVer);
@ -547,3 +556,12 @@ export function quoteIdent(ident: string): string {
assert(/^[\w.]+$/.test(ident), `SQL identifier is not valid: ${ident}`); assert(/^[\w.]+$/.test(ident), `SQL identifier is not valid: ${ident}`);
return `"${ident}"`; return `"${ident}"`;
} }
/**
* Limit the number of ATTACHed databases permitted.
*/
export function limitAttach(db: sqlite3.Database, maxAttach: number) {
// Pardon the casts, types are out of date.
const SQLITE_LIMIT_ATTACHED = (sqlite3 as any).LIMIT_ATTACHED;
(db as any).configure('limit', SQLITE_LIMIT_ATTACHED, maxAttach);
}

View File

@ -80,7 +80,7 @@
"@gristlabs/express-session": "1.17.0", "@gristlabs/express-session": "1.17.0",
"@gristlabs/moment-guess": "1.2.4-grist.1", "@gristlabs/moment-guess": "1.2.4-grist.1",
"@gristlabs/pidusage": "2.0.17", "@gristlabs/pidusage": "2.0.17",
"@gristlabs/sqlite3": "4.1.1-grist.4", "@gristlabs/sqlite3": "4.1.1-grist.6",
"@popperjs/core": "2.3.3", "@popperjs/core": "2.3.3",
"accept-language-parser": "1.5.0", "accept-language-parser": "1.5.0",
"async-mutex": "0.2.4", "async-mutex": "0.2.4",

View File

@ -77,10 +77,10 @@
dependencies: dependencies:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
"@gristlabs/sqlite3@4.1.1-grist.4", "@gristlabs/sqlite3@^4.1.1-grist.4": "@gristlabs/sqlite3@4.1.1-grist.6", "@gristlabs/sqlite3@^4.1.1-grist.4":
version "4.1.1-grist.4" version "4.1.1-grist.6"
resolved "https://registry.yarnpkg.com/@gristlabs/sqlite3/-/sqlite3-4.1.1-grist.4.tgz#b909983a33ac66f4a086a18318389ef34c779720" resolved "https://registry.yarnpkg.com/@gristlabs/sqlite3/-/sqlite3-4.1.1-grist.6.tgz#2a63021fec708e5165d0eeba19777a8c0ea3170d"
integrity sha512-/PzqeZJm9bNaqP1hsj3bt0E5q1VFdZHDnqzLYxSJzDl0rxVdGkkOmnlW4cAwjyRCMyvpeXBVAY6U4BS2xehHSg== integrity sha512-XtkwvnicrgcbncraS+IkZtDS4mUplJG+8nzJp5cX0Q7NFhrigjzmXX59aJMx7dGrCBuR6GCmlszRCiONm5IMrQ==
dependencies: dependencies:
node-addon-api "2.0.0" node-addon-api "2.0.0"
node-pre-gyp "^0.11.0" node-pre-gyp "^0.11.0"