fix: stop logging password reset secrets

This commit is contained in:
sirily
2026-03-11 13:28:18 +03:00
parent f404c36ed1
commit 348f197d99
4 changed files with 55 additions and 17 deletions

View File

@@ -23,6 +23,8 @@ Deploy on one VPS with Docker Compose.
- Keep secrets in server-side environment files or a secret manager. - Keep secrets in server-side environment files or a secret manager.
- Back up PostgreSQL and object storage separately. - Back up PostgreSQL and object storage separately.
- Prefer Telegram long polling to avoid an extra public webhook surface for the bot. - Prefer Telegram long polling to avoid an extra public webhook surface for the bot.
- In non-production environments, set `EMAIL_PROVIDER=example` only when you explicitly want the built-in debug transport. It logs redacted email previews and must never emit live password-reset tokens.
- Do not rely on implicit email fallbacks. Unsupported providers now fail fast at startup so misconfigured deployments do not silently drop password-reset or billing mail.
## Upgrade strategy ## Upgrade strategy
- Build new images. - Build new images.

View File

@@ -16,7 +16,9 @@
], ],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"check": "tsc -p tsconfig.json --noEmit" "check": "tsc -p tsconfig.json --noEmit",
"pretest": "pnpm build",
"test": "node --test dist/**/*.test.js"
}, },
"dependencies": { "dependencies": {
"@nproxy/domain": "workspace:*" "@nproxy/domain": "workspace:*"

View File

@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createEmailTransport } from "./email.js";
test("example email transport redacts password-reset tokens before logging", async () => {
const transport = createEmailTransport({
provider: "example",
from: "noreply@nproxy.test",
apiKey: "unused",
});
const logMessages: string[] = [];
const originalConsoleLog = console.log;
console.log = (message?: unknown) => {
logMessages.push(String(message ?? ""));
};
try {
await transport.send({
to: "user@example.com",
subject: "Reset your password",
text: "Reset link: https://app.nproxy.test/reset-password?token=secret-token-123",
});
} finally {
console.log = originalConsoleLog;
}
assert.equal(logMessages.length, 1);
assert.match(logMessages[0] ?? "", /"mode":"debug_redacted"/);
assert.match(logMessages[0] ?? "", /token=\[REDACTED\]/);
assert.doesNotMatch(logMessages[0] ?? "", /secret-token-123/);
});
test("unsupported email providers fail closed", () => {
assert.throws(
() =>
createEmailTransport({
provider: "smtp",
from: "noreply@nproxy.test",
apiKey: "unused",
}),
/Unsupported email provider: smtp/,
);
});

View File

@@ -20,29 +20,20 @@ export function createEmailTransport(config: {
JSON.stringify({ JSON.stringify({
service: "email", service: "email",
provider: config.provider, provider: config.provider,
mode: "debug_redacted",
from: config.from, from: config.from,
to: input.to, to: input.to,
subject: input.subject, subject: input.subject,
text: input.text, textPreview: redactEmailText(input.text),
}), }),
); );
}, },
}; };
} }
return { throw new Error(`Unsupported email provider: ${config.provider}`);
async send(input) { }
console.log(
JSON.stringify({ function redactEmailText(text: string): string {
service: "email", return text.replace(/([?&]token=)[^&\s]+/gi, "$1[REDACTED]");
provider: config.provider,
mode: "noop_fallback",
from: config.from,
to: input.to,
subject: input.subject,
text: input.text,
}),
);
},
};
} }