Skip to content

Commit bd20c8f

Browse files
committed
fixup! sqlite: reject connection access from authorizer callbacks
Cover the guard paths the existing tests never reached: the authorizer guards on enableDefensive() and loadExtension(), the stepping guard on statement[Symbol.dispose](), and the stepping guards on the tag store's get(), all(), and iterate(). The tag store test looped over the four outer driver methods but always reentered through run(), so three of its four guards never fired. loadExtension() checks that extension loading is enabled before the authorizer guard, so its test opens the database with allowExtension. Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
1 parent ca8ee11 commit bd20c8f

2 files changed

Lines changed: 85 additions & 35 deletions

File tree

‎test/parallel/test-sqlite-authz.js‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,26 @@ suite('authorizer callback reentrancy', () => {
344344
function: () => db.function('noop', () => 1),
345345
aggregate: () => db.aggregate('agg', { start: 0, step: (acc) => acc }),
346346
enableLoadExtension: () => db.enableLoadExtension(false),
347+
enableDefensive: () => db.enableDefensive(true),
347348
limits: () => { db.limits.length = 100; },
348349
};
349350

350351
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
351352
});
352353

354+
// loadExtension() checks that extension loading is enabled before reaching
355+
// the authorizer guard, so it needs a database opened with allowExtension.
356+
it('rejects loadExtension', () => {
357+
const db = new DatabaseSync(':memory:', { allowExtension: true });
358+
db.enableLoadExtension(true);
359+
db.exec('CREATE TABLE t (x INTEGER)');
360+
const cases = {
361+
loadExtension: () => db.loadExtension('/nonexistent/extension'),
362+
};
363+
364+
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
365+
});
366+
353367
// close() and deserialize() tear down the connection, so the pre-existing
354368
// callback depth guard already rejects them with its own message.
355369
it('rejects methods the callback depth guard already covers', () => {
@@ -461,6 +475,36 @@ suite('authorizer callback reentrancy', () => {
461475
assert.strictEqual(outcome, steppingError);
462476
});
463477

478+
// Unlike an already-finalized statement, disposing the one being stepped
479+
// would free the running virtual machine, so it throws.
480+
it('rejects disposing the statement being stepped', () => {
481+
const db = new DatabaseSync(':memory:');
482+
db.exec('CREATE TABLE t (x INTEGER)');
483+
db.exec('INSERT INTO t VALUES (1)');
484+
const stmt = db.prepare('SELECT x FROM t');
485+
stmt.get();
486+
db.exec('ALTER TABLE t ADD COLUMN y INTEGER');
487+
488+
let outcome = 'authorizer callback did not run';
489+
let ran = false;
490+
db.setAuthorizer(() => {
491+
if (!ran) {
492+
ran = true;
493+
try {
494+
stmt[Symbol.dispose]();
495+
outcome = 'did not throw';
496+
} catch (err) {
497+
outcome = `${err.code}: ${err.message}`;
498+
}
499+
}
500+
return constants.SQLITE_OK;
501+
});
502+
503+
stmt.get();
504+
505+
assert.strictEqual(outcome, steppingError);
506+
});
507+
464508
it('rejects iterator methods', () => {
465509
const db = new DatabaseSync(':memory:');
466510
db.exec('CREATE TABLE t (x INTEGER)');

‎test/parallel/test-sqlite-udf-close.js‎

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -114,47 +114,53 @@ for (const method of ['all', 'get', 'run', 'iterate']) {
114114
}
115115

116116
// Tag store methods resolve to a cached statement, which may be the one
117-
// currently being stepped.
118-
test(`tag store reentry during statement.${method}()`, () => {
119-
const db = new DatabaseSync(':memory:');
120-
const sql = db.createTagStore(10);
121-
db.exec(`
122-
CREATE TABLE data (value INTEGER, padding TEXT);
123-
INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
124-
(2, '${'y'.repeat(400)}');
125-
`);
117+
// currently being stepped. Each reentrant method has its own guard, so all
118+
// four are exercised.
119+
for (const reentrant of ['run', 'get', 'all', 'iterate']) {
120+
test(`tag store ${reentrant} reentry during statement.${method}()`, () => {
121+
const db = new DatabaseSync(':memory:');
122+
const sql = db.createTagStore(10);
123+
db.exec(`
124+
CREATE TABLE data (value INTEGER, padding TEXT);
125+
INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
126+
(2, '${'y'.repeat(400)}');
127+
`);
126128

127-
let thrown;
128-
db.function('reenter_tag', (value) => {
129-
if (thrown === undefined) {
130-
try {
131-
// The identical tagged literal resolves to the same cached
132-
// statement that is mid-execution.
133-
// eslint-disable-next-line no-unused-expressions
134-
sql.run`SELECT reenter_tag(value), padding FROM data`;
135-
thrown = null;
136-
} catch (err) {
137-
thrown = err;
129+
let thrown;
130+
db.function('reenter_tag', (value) => {
131+
if (thrown === undefined) {
132+
try {
133+
// The identical tagged literal resolves to the same cached
134+
// statement that is mid-execution.
135+
// All four reject at call time, iterate() included, so the
136+
// result is never consumed.
137+
// eslint-disable-next-line no-unused-expressions
138+
sql[reentrant]`SELECT reenter_tag(value), padding FROM data`;
139+
thrown = null;
140+
} catch (err) {
141+
thrown = err;
142+
}
138143
}
139-
}
140-
return value;
141-
});
144+
return value;
145+
});
142146

143-
if (method === 'iterate') {
144-
for (const row of sql.iterate`SELECT reenter_tag(value), padding FROM data`) {
145-
assert.ok(row);
147+
if (method === 'iterate') {
148+
for (const row of sql.iterate`SELECT reenter_tag(value), padding FROM data`) {
149+
assert.ok(row);
150+
}
151+
} else {
152+
// eslint-disable-next-line no-unused-expressions
153+
sql[method]`SELECT reenter_tag(value), padding FROM data`;
146154
}
147-
} else {
148-
// eslint-disable-next-line no-unused-expressions
149-
sql[method]`SELECT reenter_tag(value), padding FROM data`;
150-
}
151155

152-
assert.ok(thrown, 'tag store reentry was not rejected');
153-
assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
154-
assert.strictEqual(thrown.message, 'statement is already being executed');
156+
assert.ok(thrown, `tag store ${reentrant} reentry was not rejected`);
157+
assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
158+
assert.strictEqual(thrown.message,
159+
'statement is already being executed');
155160

156-
db.close();
157-
});
161+
db.close();
162+
});
163+
}
158164

159165
// A UDF may prepare and finalize its own helper statements. Only the
160166
// statement being stepped is off limits.

0 commit comments

Comments
 (0)