export: Don't leak writeable pad ID when exporting

Co-authored-by: Richard Hansen <rhansen@rhansen.org>
This commit is contained in:
webzwo0i 2021-06-29 19:13:10 +02:00 committed by Richard Hansen
parent 58bd96ce8f
commit dbd76f0c5d
4 changed files with 30 additions and 20 deletions

View file

@ -1,5 +1,19 @@
# Next Release # Next Release
### Security fixes
* Fixed leak of the writable pad ID when exporting from the pad's read-only ID.
This only matters if you treat the writeable pad IDs as secret (e.g., you are
not using [ep_padlist2](https://www.npmjs.com/package/ep_padlist2)) and you
share the pad's read-only ID with untrusted users. Instead of treating
writeable pad IDs as secret, you are encouraged to take advantage of
Etherpad's authentication and authorization mechanisms (e.g., use
[ep_openid_connect](https://www.npmjs.com/package/ep_openid_connect) with
[ep_readonly_guest](https://www.npmjs.com/package/ep_readonly_guest), or write
your own
[authentication](https://etherpad.org/doc/v1.8.14/#index_authenticate) and
[authorization](https://etherpad.org/doc/v1.8.14/#index_authorize) plugins).
### Compatibility changes ### Compatibility changes
* For plugin authors: * For plugin authors:

View file

@ -56,14 +56,14 @@ exports.doExport = async (req, res, padId, readOnlyId, type) => {
// if this is a plain text export, we can do this directly // if this is a plain text export, we can do this directly
// We have to over engineer this because tabs are stored as attributes and not plain text // We have to over engineer this because tabs are stored as attributes and not plain text
if (type === 'etherpad') { if (type === 'etherpad') {
const pad = await exportEtherpad.getPadRaw(padId); const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
res.send(pad); res.send(pad);
} else if (type === 'txt') { } else if (type === 'txt') {
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
res.send(txt); res.send(txt);
} else { } else {
// render the html document // render the html document
let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev); let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev, readOnlyId);
// decide what to do with the html export // decide what to do with the html export

View file

@ -19,23 +19,18 @@
const db = require('../db/DB'); const db = require('../db/DB');
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
exports.getPadRaw = async (padId) => { exports.getPadRaw = async (padId, readOnlyId) => {
const padKey = `pad:${padId}`; const keyPrefixRead = `pad:${padId}`;
const padcontent = await db.get(padKey); const keyPrefixWrite = readOnlyId ? `pad:${readOnlyId}` : keyPrefixRead;
const padcontent = await db.get(keyPrefixRead);
const records = [padKey]; const keySuffixes = [''];
for (let i = 0; i <= padcontent.head; i++) { for (let i = 0; i <= padcontent.head; i++) keySuffixes.push(`:revs:${i}`);
records.push(`${padKey}:revs:${i}`); for (let i = 0; i <= padcontent.chatHead; i++) keySuffixes.push(`:chat:${i}`);
}
for (let i = 0; i <= padcontent.chatHead; i++) {
records.push(`${padKey}:chat:${i}`);
}
const data = {}; const data = {};
for (const key of records) { for (const keySuffix of keySuffixes) {
// For each piece of info about a pad. const entry = data[keyPrefixWrite + keySuffix] = await db.get(keyPrefixRead + keySuffix);
const entry = data[key] = await db.get(key);
// Get the Pad Authors // Get the Pad Authors
if (entry.pool && entry.pool.numToAttrib) { if (entry.pool && entry.pool.numToAttrib) {
@ -50,7 +45,7 @@ exports.getPadRaw = async (padId) => {
if (authorEntry) { if (authorEntry) {
data[`globalAuthor:${authorId}`] = authorEntry; data[`globalAuthor:${authorId}`] = authorEntry;
if (authorEntry.padIDs) { if (authorEntry.padIDs) {
authorEntry.padIDs = padId; authorEntry.padIDs = readOnlyId || padId;
} }
} }
} }
@ -63,7 +58,8 @@ exports.getPadRaw = async (padId) => {
const prefixes = await hooks.aCallAll('exportEtherpadAdditionalContent'); const prefixes = await hooks.aCallAll('exportEtherpadAdditionalContent');
await Promise.all(prefixes.map(async (prefix) => { await Promise.all(prefixes.map(async (prefix) => {
const key = `${prefix}:${padId}`; const key = `${prefix}:${padId}`;
data[key] = await db.get(key); const writeKey = readOnlyId ? `${prefix}:${readOnlyId}` : key;
data[writeKey] = await db.get(key);
})); }));
return data; return data;

View file

@ -457,7 +457,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return pieces.join(''); return pieces.join('');
}; };
exports.getPadHTMLDocument = async (padId, revNum) => { exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// Include some Styles into the Head for Export // Include some Styles into the Head for Export
@ -475,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum) => {
return eejs.require('ep_etherpad-lite/templates/export_html.html', { return eejs.require('ep_etherpad-lite/templates/export_html.html', {
body: html, body: html,
padId: Security.escapeHTML(padId), padId: Security.escapeHTML(readOnlyId || padId),
extraCSS: stylesForExportCSS, extraCSS: stylesForExportCSS,
}); });
}; };