69754f8a2c
* fix(email): scrub gallery passwords from the sent-mail archive
The email queue kept every gallery password and client PIN in clear
text in email_data and rendered_html after the mail was sent, and the
Messages reading pane handed them back to any admin with the messaging
flag. A password hash in the events table bought nothing while the
plaintext sat next to it.
Once a mail is out, or its retries are exhausted, the processor now
masks secret-looking variables (password, passcode, pin) in email_data
and replaces their values in the rendered body, plain and HTML-escaped.
The reading pane applies the same masking to rows archived before this
change. Pending rows keep the real values so a retry still sends them.
Relates to issue 1271
* fix(email): keep a quoted ">" from cutting an attribute value out of redaction
The tag splitter stopped at the first ">", so a template attribute such as
title="{{gallery_password}} > details" left the password unmasked in the
archived HTML while email_data was already masked. The tokenizer is now
quote-aware; a tag with an unbalanced quote falls through as text and is
scrubbed there.
* fix(email): scrub secrets inside HTML comments in the archived body
A comment such as <!-- PIN: {{client_password}} --> was split off as a tag
and its body, which has no attribute, was never scrubbed. Comments are now
one segment and their content is masked whole.
---------
Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
57 lines
2.9 KiB
JavaScript
57 lines
2.9 KiB
JavaScript
/**
|
|
* The Messages reading pane must not serve passwords (see
|
|
* utils/emailSecretRedaction.js). Rows sent before the processor learned to
|
|
* scrub still carry the gallery password in email_data and rendered_html;
|
|
* the route redacts them on read.
|
|
*/
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.TEST_DATABASE_PATH = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'picpeak-paneredact-')), 'db.sqlite');
|
|
process.env.JWT_SECRET = process.env.JWT_SECRET || 'paneredact-test-secret';
|
|
process.env.STORAGE_PATH = fs.mkdtempSync(path.join(os.tmpdir(), 'picpeak-paneredact-storage-'));
|
|
|
|
const request = require('supertest');
|
|
const { bootCrmDb, seedMinimal, assignAdminRole, mintAdminToken, buildRouteApp } = require('../integration/helpers/crmDb');
|
|
const { invalidateFeatureFlagCache } = require('../../src/middleware/requireFeatureFlag');
|
|
const { MASK } = require('../../src/utils/emailSecretRedaction');
|
|
|
|
describe('GET /admin/email/queue/:id redacts secrets from legacy rows', () => {
|
|
let db; let cleanup; let app; let token; let rowId;
|
|
const PASSWORD = 'Sunset-42!'; const PIN = 'Tom & Ada\'s 7788';
|
|
|
|
beforeAll(async () => {
|
|
({ db, cleanup } = await bootCrmDb());
|
|
const { adminId } = await seedMinimal(db);
|
|
await assignAdminRole(db, adminId, 'super_admin');
|
|
token = mintAdminToken(adminId);
|
|
await db('feature_flags').insert({ key: 'messaging', value: true }).onConflict('key').merge({ value: true });
|
|
invalidateFeatureFlagCache();
|
|
const ins = await db('email_queue').insert({
|
|
recipient_email: 'client@example.com', email_type: 'gallery_created', status: 'sent',
|
|
created_at: new Date().toISOString(), sent_at: new Date().toISOString(), retry_count: 0,
|
|
email_data: JSON.stringify({ customer_name: 'Ada', gallery_password: PASSWORD, client_password: PIN, cc: ['second@example.com'] }),
|
|
rendered_html: `<ul><li>Password: ${PASSWORD}</li><li>PIN: Tom & Ada's 7788</li></ul>`,
|
|
}).returning('id');
|
|
rowId = ins[0]?.id ?? ins[0];
|
|
app = buildRouteApp('/api/admin/email', require('../../src/routes/adminEmail'));
|
|
}, 120000);
|
|
afterAll(async () => { if (cleanup) await cleanup(); });
|
|
|
|
it('masks the password and the PIN in the rendered body, keeps the rest', async () => {
|
|
const res = await request(app).get(`/api/admin/email/queue/${rowId}`).set('Authorization', `Bearer ${token}`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.renderedHtml).not.toContain(PASSWORD);
|
|
expect(res.body.renderedHtml).not.toContain('Ada's 7788');
|
|
expect(res.body.renderedHtml).toContain(`Password: ${MASK}`);
|
|
expect(res.body.renderedHtml).toContain(`PIN: ${MASK}`);
|
|
expect(res.body.cc).toBe('second@example.com');
|
|
expect(JSON.stringify(res.body)).not.toContain(PASSWORD);
|
|
// the stored row is untouched by a read
|
|
const row = await db('email_queue').where('id', rowId).first();
|
|
expect(row.rendered_html).toContain(PASSWORD);
|
|
});
|
|
});
|