Skip to content

Commit 19909ea

Browse files
committed
ImportEtherpad: Rigorously check imported data
1 parent 885ff3b commit 19909ea

File tree

3 files changed

+130
-2
lines changed

3 files changed

+130
-2
lines changed

src/node/db/PadManager.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121

2222
const CustomError = require('../utils/customError');
23-
const Pad = require('../db/Pad').Pad;
23+
const Pad = require('../db/Pad');
2424
const db = require('./DB');
2525
const hooks = require('../../static/js/pluginfw/hooks');
2626

@@ -138,7 +138,7 @@ exports.getPad = async (id, text) => {
138138
}
139139

140140
// try to load pad
141-
pad = new Pad(id);
141+
pad = new Pad.Pad(id);
142142

143143
// initialize the pad
144144
await pad.init(text);

src/node/utils/ImportEtherpad.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* limitations under the License.
1717
*/
1818

19+
const {Pad} = require('../db/Pad');
1920
const authorManager = require('../db/AuthorManager');
2021
const db = require('../db/DB');
2122
const hooks = require('../../static/js/pluginfw/hooks');
@@ -90,6 +91,21 @@ exports.setPadRaw = async (padId, r) => {
9091
dbRecords.set(key, value);
9192
}));
9293

94+
const pad = new Pad(padId, {
95+
// Only fetchers are needed to check the pad's integrity.
96+
get: async (k) => dbRecords.get(k),
97+
getSub: async (k, sub) => {
98+
let v = dbRecords.get(k);
99+
for (const sk of sub) {
100+
if (v == null) return null;
101+
v = v[sk];
102+
}
103+
return v;
104+
},
105+
});
106+
await pad.init();
107+
await pad.check();
108+
93109
await Promise.all([
94110
...[...dbRecords].map(async ([k, v]) => await db.set(k, v)),
95111
...[...existingAuthors].map(async (authorId) => await authorManager.addPad(authorId, padId)),

src/tests/backend/specs/api/importexportGetPost.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,118 @@ describe(__filename, function () {
315315
});
316316
});
317317

318+
describe('malformed .etherpad files are rejected', function () {
319+
const makeGoodExport = () => ({
320+
'pad:testing': {
321+
atext: {
322+
text: 'foo\n',
323+
attribs: '|1+4',
324+
},
325+
pool: {
326+
numToAttrib: {
327+
0: ['author', 'a.foo'],
328+
},
329+
nextNum: 1,
330+
},
331+
head: 0,
332+
savedRevisions: [],
333+
},
334+
'globalAuthor:a.foo': {
335+
colorId: '#000000',
336+
name: 'author foo',
337+
timestamp: 1598747784631,
338+
padIDs: 'testing',
339+
},
340+
'pad:testing:revs:0': {
341+
changeset: 'Z:1>3+3$foo',
342+
meta: {
343+
author: 'a.foo',
344+
timestamp: 1597632398288,
345+
pool: {
346+
numToAttrib: {},
347+
nextNum: 0,
348+
},
349+
atext: {
350+
text: 'foo\n',
351+
attribs: '|1+4',
352+
},
353+
},
354+
},
355+
});
356+
357+
const importEtherpad = (records) => agent.post(`/p/${testPadId}/import`)
358+
.attach('file', Buffer.from(JSON.stringify(records), 'utf8'), {
359+
filename: '/test.etherpad',
360+
contentType: 'application/etherpad',
361+
});
362+
363+
before(async function () {
364+
// makeGoodExport() is assumed to produce good .etherpad records. Verify that assumption so
365+
// that a buggy makeGoodExport() doesn't cause checks to accidentally pass.
366+
const records = makeGoodExport();
367+
await importEtherpad(records)
368+
.expect(200)
369+
.expect('Content-Type', /json/)
370+
.expect((res) => assert.deepEqual(res.body, {
371+
code: 0,
372+
message: 'ok',
373+
data: {directDatabaseAccess: true},
374+
}));
375+
await agent.get(`/p/${testPadId}/export/txt`)
376+
.expect(200)
377+
.buffer(true).parse(superagent.parse.text)
378+
.expect((res) => assert.match(res.text, /foo/));
379+
});
380+
381+
it('missing rev', async function () {
382+
const records = makeGoodExport();
383+
delete records['pad:testing:revs:0'];
384+
await importEtherpad(records).expect(500);
385+
});
386+
387+
it('bad changeset', async function () {
388+
const records = makeGoodExport();
389+
records['pad:testing:revs:0'].changeset = 'garbage';
390+
await importEtherpad(records).expect(500);
391+
});
392+
393+
it('missing attrib in pool', async function () {
394+
const records = makeGoodExport();
395+
records['pad:testing'].pool.nextNum++;
396+
await importEtherpad(records).expect(500);
397+
});
398+
399+
it('extra attrib in pool', async function () {
400+
const records = makeGoodExport();
401+
const pool = records['pad:testing'].pool;
402+
pool.numToAttrib[pool.nextNum] = ['key', 'value'];
403+
await importEtherpad(records).expect(500);
404+
});
405+
406+
it('changeset refers to non-existent attrib', async function () {
407+
const records = makeGoodExport();
408+
records['pad:testing:revs:1'] = {
409+
changeset: 'Z:4>4*1+4$asdf',
410+
meta: {
411+
author: 'a.foo',
412+
timestamp: 1597632398288,
413+
},
414+
};
415+
records['pad:testing'].head = 1;
416+
records['pad:testing'].atext = {
417+
text: 'asdffoo\n',
418+
attribs: '*1+4|1+4',
419+
};
420+
await importEtherpad(records).expect(500);
421+
});
422+
423+
it('pad atext does not match', async function () {
424+
const records = makeGoodExport();
425+
records['pad:testing'].atext.attribs = `*0${records['pad:testing'].atext.attribs}`;
426+
await importEtherpad(records).expect(500);
427+
});
428+
});
429+
318430
describe('Import authorization checks', function () {
319431
let authorize;
320432

0 commit comments

Comments
 (0)