SQL layer (PostgreSQL): Pg#deleteEqual, deleteEqualAnd and deleteLowerThan reject the value 0 (and ""), which the SQLite backend deletes normally
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 62/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- postgresql, sqlite, typescript
- Domain
- databases
Research direction
Start with the truthiness guards !value || value.toString().length === 0 in src/modules/legacy/matrix-identity-server/db/sql/pg.ts (deleteEqual, deleteEqualAnd, deleteLowerThan) and the same expressions in src/modules/legacy/db/sql/pg.ts, then compare with the SQLite delete methods, which have no such guard. Add the regression test from the issue, using a faked pool, so that value 0 binds as $1 and resolves. The open decision is what to do with empty strings, so confirm that choice with the maintainers before changing "" handling.
Written by the indexing model from the issue text.
Description
Steps to reproduce
The two backends behave differently on the same library call (no HTTP request is needed). The fuzzer found three variants on an empty invitations table:
await db.deleteEqual('invitations', 'id', 0) // matrix-identity-server Pg
await db.deleteLowerThan('invitations', 'id', 0) // matrix-identity-server Pg
await db.deleteEqualAnd('invitations', { field: 'id', value: 0 }, // TwakeDB Pg (src/modules/legacy/db/sql/pg.ts)
{ field: 'id', value: 0 })
SQLite resolves all three. Pg rejects all three before sending any SQL. The same happens with ''.
Fuzzer input (scenario 33-sql-backend-consistency, 5 bytes, hex): 3c 08 00 2b 80
Regression test (bun:test, e.g. src/modules/legacy/matrix-identity-server/db/sql/pg.test.ts; the pool is faked, so no PostgreSQL is needed):
import { describe, expect, it } from 'bun:test'
import Pg from './pg'
const makePg = () => {
const queries: Array<{ text: string; values: unknown[] }> = []
const noop = (): void => {}
const pg = Object.create(Pg.prototype) as Pg<string>
Object.assign(pg, {
logger: { debug: noop, info: noop, warn: noop, error: noop },
db: {
query: (text: string, values: unknown[], cb: (e: Error | null, r: { rows: unknown[] }) => void) => {
queries.push({ text, values })
cb(null, { rows: [] })
}
}
})
return { pg, queries }
}
describe('Pg delete methods accept the value 0', () => {
it('deleteEqual', async () => {
const { pg, queries } = makePg()
await expect(pg.deleteEqual('invitations', 'id', 0)).resolves.toBeUndefined()
expect(queries).toEqual([{ text: 'DELETE FROM invitations WHERE id=$1', values: [0] }])
})
it('deleteLowerThan', async () => {
const { pg } = makePg()
await expect(pg.deleteLowerThan('invitations', 'expiration', 0)).resolves.toBeUndefined()
})
it('deleteEqualAnd', async () => {
const { pg } = makePg()
await expect(
pg.deleteEqualAnd('invitations', { field: 'id', value: 0 }, { field: 'sender', value: 0 })
).resolves.toBeUndefined()
})
})
Add the same test for src/modules/legacy/db/sql/pg.ts.
Expected
Both backends treat the same value the same way. 0 is a valid string | number value and is bound as $1. For "", the maintainers should pick one behaviour (reject it as an invalid filter, or run WHERE x='') and apply it to both Pg and SQLite.
Observed
deleteEqual: SQLite resolved but Pg rejected (Bad deleteEqual call, field: id, value: 0)
deleteLowerThan: SQLite resolved but Pg rejected (Bad deleteLowerThan call, field: id, value: 0)
deleteEqualAnd: SQLite resolved but Pg rejected (Bad deleteAnd call, conditions: id=0, id=0) [TwakeDB copy]
In each case no statement reached the pool (pgStatements: []).
Possible cause
The parameter guards test the values for truthiness:
src/modules/legacy/matrix-identity-server/db/sql/pg.ts:889(deleteEqual),:954and:958(deleteEqualAnd),:1022(deleteLowerThan):!value || value.toString().length === 0src/modules/legacy/db/sql/pg.ts:809,:868/:872and:928: the same expressions.
!value is true for 0. The SQLite classes (sqlite.ts deleteEqual / deleteEqualAnd / deleteLowerThan) have no such guard. Suggested fix: replace !value with value == null, and move one shared guard into the SQL base class (or a helper used by both backends), so that "" is handled the same way on both.
Introduced by
Not found: the guards and the whole pg.ts files date from 4d406b7 (2026-09-15, "chore: initial import"), a squashed import of the code with no earlier history.
Impact
This is a backend divergence and is not reachable by any client today. The production callers (changePepper, logout, unbind, the address book and invitation services, the token cleanup deleteLowerThan(table, 'expires', epoch())) pass non-empty strings or a current timestamp, never 0. If an empty string reaches one of them (hypothesis: an empty id or mxid), PostgreSQL deployments reject the call and the handler reports an error, while SQLite deployments and the test suite run the DELETE. The Pg files are marked istanbul ignore file and have no unit tests, so this kind of difference only shows up in production. There is no confidentiality or integrity impact: the PostgreSQL side deletes less, not more.
Fuzzing scenario
33-sql-backend-consistency (runs the same operation sequences on the Pg and SQLite query builders and compares results and table contents). It produced 3 crash inputs for this bug (deleteEqual, deleteLowerThan, deleteEqualAnd) in a 900 s run on commit e2e64af072bd45835ff6738234014135b4f3793d. The run's log does not report the execution count at the first crash.
Found by twake-fuzz (coverage-guided fuzzing), scenario 33-sql-backend-consistency, commit e2e64af.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 3
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 19
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from linagora/ToM-Server
-
bug claude Minor static-analysis
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#126 ·
Maintainers usually reply within 1 day
-
bug claude Minor static-analysis
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
linagora/ToM-Server#125 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#123 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
linagora/ToM-Server#118 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#114 ·
Maintainers usually reply within 1 day
All issues in linagora/ToM-Server
Similar issues
-
Add "append" and "prepend" slots for vue-vuetify extended renderersPossibly taken @simeyc claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
eclipsesource/jsonforms#2668 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
README の通知条件を実際の判定処理に合わせるPossibly taken @unhappychoice claimed this today. Openfactory:in-progress priority:medium
Difficulty 1/5 Under an hour Newbie friendliness 90/100
unhappychoice/oss-issue-opener#395 · 1 comment ·
Maintainers usually reply within 1 day
-
bug Website
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
OWASP/OWASP-Hunting#88 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day