Skip to content

Commit

Permalink
fix: fixed alias token verification, wrapped resolveTxt with try/catc…
Browse files Browse the repository at this point in the history
…h, added and updated tests
  • Loading branch information
titanism committed Jul 13, 2023
1 parent cc1fb5f commit 923bc57
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 7 deletions.
22 changes: 16 additions & 6 deletions smtp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,15 @@ async function onAuth(auth, session, fn) {
);

const verifications = [];
const records = await this.resolver.resolveTxt(domainName);
for (const record_ of records) {
const record = record_.join('').trim(); // join chunks together
if (record.startsWith(config.paidPrefix))
verifications.push(record.replace(config.paidPrefix, '').trim());
try {
const records = await this.resolver.resolveTxt(domainName);
for (const record_ of records) {
const record = record_.join('').trim(); // join chunks together
if (record.startsWith(config.paidPrefix))
verifications.push(record.replace(config.paidPrefix, '').trim());
}
} catch (err) {
logger.error(err, { session });
}

if (verifications.length === 0)
Expand Down Expand Up @@ -541,6 +545,7 @@ async function onAuth(auth, session, fn) {
'user',
`id ${config.userFields.isBanned} ${config.userFields.smtpLimit}`
)
.select('+tokens.hash +tokens.salt')
.lean()
.exec();

Expand Down Expand Up @@ -583,7 +588,12 @@ async function onAuth(auth, session, fn) {
}

// ensure that the token is valid
if (!Aliases.isValidPassword(alias.tokens, auth.password.trim()))
const isValid = await Aliases.isValidPassword(
alias.tokens,
auth.password.trim()
);

if (!isValid)
throw new SMTPError(
`Invalid password, please try again or go to ${config.urls.web}/my-account/domains/${domainName}/aliases and click "Generate Password"`,
{
Expand Down
157 changes: 156 additions & 1 deletion test/smtp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const createTangerine = require('#helpers/create-tangerine');
const env = require('#config/env');
const logger = require('#helpers/logger');
const processEmail = require('#helpers/process-email');
const { Emails } = require('#models');
const { Emails, Aliases } = require('#models');

const asyncMxConnect = pify(mxConnect);
const IP_ADDRESS = ip.address();
Expand Down Expand Up @@ -185,6 +185,157 @@ Test`.trim()
}
});

test('auth with pass as alias', async (t) => {
const smtp = new SMTP({ client: t.context.client }, true);
const port = await getPort();
await smtp.listen(port);

const user = await factory.create('user', {
plan: 'enhanced_protection',
[config.userFields.planSetAt]: dayjs().startOf('day').toDate()
});

await factory.create('payment', {
user: user._id,
amount: 300,
invoice_at: dayjs().startOf('day').toDate(),
method: 'free_beta_program',
duration: ms('30d'),
plan: user.plan,
kind: 'one-time'
});

await user.save();

const resolver = createTangerine(t.context.client, logger);

const domain = await factory.create('domain', {
members: [{ user: user._id, group: 'admin' }],
plan: user.plan,
resolver,
has_smtp: true
});

const alias = await factory.create('alias', {
user: user._id,
domain: domain._id,
recipients: [user.email]
});

await alias.createToken();
await alias.save();

// spoof dns records
const map = new Map();

map.set(
`txt:${domain.name}`,
resolver.spoofPacket(
domain.name,
'TXT',
[`${config.paidPrefix}${domain.verification_record}`],
true
)
);

// dkim
map.set(
`txt:${domain.dkim_key_selector}._domainkey.${domain.name}`,
resolver.spoofPacket(
`${domain.dkim_key_selector}._domainkey.${domain.name}`,
'TXT',
[`v=DKIM1; k=rsa; p=${domain.dkim_public_key.toString('base64')};`],
true
)
);

// spf
map.set(
`txt:${env.WEB_HOST}`,
resolver.spoofPacket(
`${env.WEB_HOST}`,
'TXT',
[`v=spf1 ip4:${IP_ADDRESS} -all`],
true
)
);

// cname
map.set(
`cname:${domain.return_path}.${domain.name}`,
resolver.spoofPacket(
`${domain.return_path}.${domain.name}`,
'CNAME',
[env.WEB_HOST],
true
)
);

// cname -> txt
map.set(
`txt:${domain.return_path}.${domain.name}`,
resolver.spoofPacket(
`${domain.return_path}.${domain.name}`,
'TXT',
[`v=spf1 ip4:${IP_ADDRESS} -all`],
true
)
);

// dmarc
map.set(
`txt:_dmarc.${domain.name}`,
resolver.spoofPacket(
`_dmarc.${domain.name}`,
'TXT',
[
// TODO: consume dmarc reports and parse dmarc-$domain
`v=DMARC1; p=reject; pct=100; rua=mailto:dmarc-${domain.id}@forwardemail.net;`
],
true
)
);

// store spoofed dns cache
await resolver.options.cache.mset(map);

const transporter = nodemailer.createTransport({
logger,
debug: true,
host: IP_ADDRESS,
port,
ignoreTLS: true,
secure: true,
tls: {
rejectUnauthorized: false
},
auth: {
user: `${alias.name}@${domain.name}`,
pass: 'test'
}
});

const err = await t.throwsAsync(
transporter.sendMail({
envelope: {
from: `${alias.name}@${domain.name}`,
to: '[email protected]'
},
raw: `
To: [email protected]
From: [email protected]
Subject: test
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Test`.trim()
})
);

t.is(err.responseCode, 535);
t.regex(err.message, /Invalid password/);
});

test('smtp outbound auth', async (t) => {
const smtp = new SMTP({ client: t.context.client }, false);
const { resolver } = smtp;
Expand Down Expand Up @@ -225,6 +376,10 @@ test('smtp outbound auth', async (t) => {
t.true(typeof pass === 'string' && pass.length === 24);
await alias.save();

const isValid = await Aliases.isValidPassword(alias.tokens, pass);

t.true(isValid);

const combos = [
{ user: 'a', pass: 'a' },
{ user: `beep@${domain.name}`, pass },
Expand Down

0 comments on commit 923bc57

Please sign in to comment.