Security: Fix revision parsing (#5772)

A carefully crated URL can cause Etherpad to hang.
This commit is contained in:
John McLear 2023-06-26 18:17:06 +01:00 committed by GitHub
parent 1d289520eb
commit 1e98033632
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 325 additions and 29 deletions

View file

@ -447,6 +447,175 @@ describe(__filename, function () {
});
});
describe('revisions are supported in txt and html export', function () {
const makeGoodExport = () => ({
'pad:testing': {
atext: {
text: 'oofoo\n',
attribs: '|1+6',
},
pool: {
numToAttrib: {
0: ['author', 'a.foo'],
},
nextNum: 1,
},
head: 2,
savedRevisions: [],
},
'globalAuthor:a.foo': {
colorId: '#000000',
name: 'author foo',
timestamp: 1598747784631,
padIDs: 'testing',
},
'pad:testing:revs:0': {
changeset: 'Z:1>3+3$foo',
meta: {
author: 'a.foo',
timestamp: 1597632398288,
pool: {
nextNum: 1,
numToAttrib: {
0: ['author', 'a.foo'],
},
},
atext: {
text: 'foo\n',
attribs: '|1+4',
},
},
},
'pad:testing:revs:1': {
changeset: 'Z:4>1+1$o',
meta: {
author: 'a.foo',
timestamp: 1597632398288,
pool: {
nextNum: 1,
numToAttrib: {
0: ['author', 'a.foo'],
},
},
atext: {
text: 'fooo\n',
attribs: '*0|1+5',
},
},
},
'pad:testing:revs:2': {
changeset: 'Z:5>1+1$o',
meta: {
author: 'a.foo',
timestamp: 1597632398288,
pool: {
numToAttrib: {},
nextNum: 0,
},
atext: {
text: 'foooo\n',
attribs: '*0|1+6',
},
},
},
});
const importEtherpad = (records) => agent.post(`/p/${testPadId}/import`)
.attach('file', Buffer.from(JSON.stringify(records), 'utf8'), {
filename: '/test.etherpad',
contentType: 'application/etherpad',
});
before(async function () {
// makeGoodExport() is assumed to produce good .etherpad records. Verify that assumption so
// that a buggy makeGoodExport() doesn't cause checks to accidentally pass.
const records = makeGoodExport();
await deleteTestPad();
await importEtherpad(records)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => assert.deepEqual(res.body, {
code: 0,
message: 'ok',
data: {directDatabaseAccess: true},
}));
await agent.get(`/p/${testPadId}/export/txt`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.equal(res.text, 'oofoo\n'));
});
it('txt request rev 1', async function () {
await agent.get(`/p/${testPadId}/1/export/txt`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.equal(res.text, 'ofoo\n'));
});
it('txt request rev 2', async function () {
await agent.get(`/p/${testPadId}/2/export/txt`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.equal(res.text, 'oofoo\n'));
});
it('txt request rev 1test returns rev 1', async function () {
await agent.get(`/p/${testPadId}/1test/export/txt`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.equal(res.text, 'ofoo\n'));
});
it('txt request rev test1 is 403', async function () {
await agent.get(`/p/${testPadId}/test1/export/txt`)
.expect(500)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.match(res.text, /rev is not a number/));
});
it('txt request rev 5 returns head rev', async function () {
await agent.get(`/p/${testPadId}/5/export/txt`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.equal(res.text, 'oofoo\n'));
});
it('html request rev 1', async function () {
await agent.get(`/p/${testPadId}/1/export/html`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.match(res.text, /ofoo<br>/));
});
it('html request rev 2', async function () {
await agent.get(`/p/${testPadId}/2/export/html`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.match(res.text, /oofoo<br>/));
});
it('html request rev 1test returns rev 1', async function () {
await agent.get(`/p/${testPadId}/1test/export/html`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.match(res.text, /ofoo<br>/));
});
it('html request rev test1 results in 500 response', async function () {
await agent.get(`/p/${testPadId}/test1/export/html`)
.expect(500)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.match(res.text, /rev is not a number/));
});
it('html request rev 5 returns head rev', async function () {
await agent.get(`/p/${testPadId}/5/export/html`)
.expect(200)
.buffer(true).parse(superagent.parse.text)
.expect((res) => assert.match(res.text, /oofoo<br>/));
});
});
describe('Import authorization checks', function () {
let authorize;

View file

@ -77,6 +77,89 @@ describe(__filename, function () {
await otherPad.remove();
}
});
it('CHANGESET_REQ: verify revNum is a number (regression)', async function () {
const otherPadId = `${padId}other`;
assert(!await padManager.doesPadExist(otherPadId));
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
let errorCatched = 0;
try {
await otherPad.setText('other text\n');
await common.sendMessage(roSocket, {
component: 'pad',
padId: otherPadId, // The server should ignore this.
type: 'CHANGESET_REQ',
data: {
granularity: 1,
start: 'test123',
requestID: 'requestId',
},
});
assert.equal('This code should never run', 1);
}
catch(e) {
assert.match(e.message, /rev is not a number/);
errorCatched = 1;
}
finally {
await otherPad.remove();
assert.equal(errorCatched, 1);
}
});
it('CHANGESET_REQ: revNum is converted to number if possible (regression)', async function () {
const otherPadId = `${padId}other`;
assert(!await padManager.doesPadExist(otherPadId));
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
try {
await otherPad.setText('other text\n');
const resP = common.waitForSocketEvent(roSocket, 'message');
await common.sendMessage(roSocket, {
component: 'pad',
padId: otherPadId, // The server should ignore this.
type: 'CHANGESET_REQ',
data: {
granularity: 1,
start: '1test123',
requestID: 'requestId',
},
});
const res = await resP;
assert.equal(res.type, 'CHANGESET_REQ');
assert.equal(res.data.requestID, 'requestId');
assert.equal(res.data.start, 1);
}
finally {
await otherPad.remove();
}
});
it('CHANGESET_REQ: revNum 2 is converted to head rev 1 (regression)', async function () {
const otherPadId = `${padId}other`;
assert(!await padManager.doesPadExist(otherPadId));
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
try {
await otherPad.setText('other text\n');
const resP = common.waitForSocketEvent(roSocket, 'message');
await common.sendMessage(roSocket, {
component: 'pad',
padId: otherPadId, // The server should ignore this.
type: 'CHANGESET_REQ',
data: {
granularity: 1,
start: '2',
requestID: 'requestId',
},
});
const res = await resP;
assert.equal(res.type, 'CHANGESET_REQ');
assert.equal(res.data.requestID, 'requestId');
assert.equal(res.data.start, 1);
}
finally {
await otherPad.remove();
}
});
});
describe('USER_CHANGES', function () {