mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 00:06:53 +00:00
fix: keep every provider write of device sync inside its own account #34
The native side ANDs the account's scope onto each selection the engine sends, as `(where) AND (scope)`. A selection such as `1) OR (1` closed the scope's parenthesis itself: on the calendar tables, where CalendarProvider adds no account filter of its own, an update or delete built that way would have reached the events, attendees and calendars of other accounts, e.g. the user's Google calendar. The engine only sends constant selections, so this guards against bugs rather than anything an outside app could send, which is what the scope is for. - Selections and sort orders must balance their parentheses outside quoted literals, and may not end the statement, carry comments or query other tables (`SELECT`, `UNION`, `ATTACH`, `PRAGMA`). - An update of a data row by id that writes `data1` without restating the mimetype has the row's mimetype looked up: on a membership row `data1` names a group, which must be the account's, as on insert. Such writes by selection are refused. - `data14`, a photo row's file id, is never written: another account's file id would have let `readPhoto` read that account's photo. Checked: 58 JVM tests (`gradlew :app:testDebugUnitTest`, three new), the debug build, and every selection the engine and planners send against the new rules.
This commit is contained in:
@@ -23,6 +23,8 @@ private class FakeLookup(private val me: AccountRef = ME) : ScopeLookup {
|
||||
val colors = HashMap<Long, AccountRef>()
|
||||
/** data id → raw contact */
|
||||
val data = HashMap<Long, Long>()
|
||||
/** data id → mimetype */
|
||||
val mimetypes = HashMap<Long, String>()
|
||||
/** event id → calendar */
|
||||
val events = HashMap<Long, Long>()
|
||||
/** child table → child id → event */
|
||||
@@ -42,6 +44,8 @@ private class FakeLookup(private val me: AccountRef = ME) : ScopeLookup {
|
||||
|
||||
override fun dataRawContacts(ids: Set<Long>) = data.filterKeys { it in ids }
|
||||
|
||||
override fun dataMimetypes(ids: Set<Long>) = mimetypes.filterKeys { it in ids }
|
||||
|
||||
override fun eventCalendars(ids: Set<Long>) = events.filterKeys { it in ids }
|
||||
|
||||
override fun childEvents(table: ProviderTable, ids: Set<Long>): Map<Long, Long> =
|
||||
@@ -306,6 +310,49 @@ class ProviderOpsTest {
|
||||
plan(CONTACTS, """{"op":"insert","table":"data","values":{"raw_contact_id":1,"mimetype":"x","data1":11}}""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a membership row's group is checked on updates by id too`() {
|
||||
lookup.mimetypes[100] = GROUP_MEMBERSHIP_MIMETYPE
|
||||
plan(CONTACTS, """{"op":"update","table":"data","id":100,"values":{"data1":10}}""")
|
||||
refused(BatchFailure.SCOPE) { plan(CONTACTS, """{"op":"update","table":"data","id":100,"values":{"data1":11}}""") }
|
||||
refused(BatchFailure.ASSERT) { plan(CONTACTS, """{"op":"update","table":"data","id":100,"values":{"data1":12}}""") }
|
||||
// data1 by a selection could reach memberships the lookup never saw.
|
||||
refused(BatchFailure.SCOPE) {
|
||||
plan(CONTACTS, """{"op":"update","table":"data","where":"raw_contact_id = ?","args":[1],"values":{"data1":11}}""")
|
||||
}
|
||||
// data1 of other kinds is plain data.
|
||||
lookup.mimetypes[100] = "vnd.android.cursor.item/phone_v2"
|
||||
plan(CONTACTS, """{"op":"update","table":"data","id":100,"values":{"data1":11}}""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `photo file ids are never written`() {
|
||||
val photo = """"raw_contact_id":1,"mimetype":"vnd.android.cursor.item/photo""""
|
||||
plan(CONTACTS, """{"op":"insert","table":"data","values":{$photo,"data14":null,"data15":{"b64":"AAAA"}}}""")
|
||||
refused(BatchFailure.SCOPE) { plan(CONTACTS, """{"op":"insert","table":"data","values":{$photo,"data14":5}}""") }
|
||||
refused(BatchFailure.SCOPE) { plan(CONTACTS, """{"op":"update","table":"data","id":100,"values":{"data14":5}}""") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a selection can't close the scope's parentheses or query elsewhere`() {
|
||||
for (where in listOf("1) OR (1", "dirty = 1) OR (1 = 1", "(1", "1; DELETE FROM events", "1 -- x", "1 /* x */",
|
||||
"_id IN (SELECT _id FROM events)", "1 UNION ALL x", "title = 'open")) {
|
||||
val quoted = JSONObject.quote(where)
|
||||
refused(BatchFailure.SCOPE) { plan(CALENDAR, """{"op":"update","table":"events","where":$quoted,"values":{"dirty":1}}""") }
|
||||
refused(BatchFailure.SCOPE) { plan(CALENDAR, """{"op":"delete","table":"attendees","where":$quoted}""") }
|
||||
refused(BatchFailure.SCOPE) {
|
||||
ProviderOpParser.parseQuery(CALENDAR, """{"table":"attendees","columns":["_id"],"where":$quoted}""")
|
||||
}
|
||||
}
|
||||
refused(BatchFailure.SCOPE) {
|
||||
ProviderOpParser.parseQuery(CONTACTS, """{"table":"raw_contacts","columns":["_id"],"orderBy":"_id); DROP TABLE x; --"}""")
|
||||
}
|
||||
// Parentheses and keywords inside quoted literals are data.
|
||||
plan(CALENDAR, """{"op":"update","table":"events","where":"title = '(x' OR title = 'select'","values":{"dirty":1}}""")
|
||||
plan(CALENDAR, """{"op":"assert","table":"events","where":"(dirty = 1 OR deleted = 1) AND _sync_id LIKE '~pending/%'","expectCount":0}""")
|
||||
ProviderOpParser.parseQuery(CONTACTS, """{"table":"raw_contacts","columns":["_id"],"orderBy":"_id DESC"}""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `moving a row to another parent is checked like an insert`() {
|
||||
refused(BatchFailure.SCOPE) { plan(CALENDAR, """{"op":"update","table":"events","id":30,"values":{"calendar_id":22}}""") }
|
||||
|
||||
Reference in New Issue
Block a user