From af7f5e1d64e7e6cac8808bb46b96c601016765ef Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 15:11:28 +0800 Subject: [PATCH] Improvements --- api/get-whois.js | 3 ++- api/sentry-tunnel.js | 14 ++++++++++++-- common/rdap.js | 1 + frontend/components/User.vue | 7 +++++++ frontend/firebase-init.js | 7 ++++++- tests/sentry-tunnel.test.js | 18 ++++++++++++++++++ 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/api/get-whois.js b/api/get-whois.js index 99ea6046..279c5a4b 100644 --- a/api/get-whois.js +++ b/api/get-whois.js @@ -22,7 +22,8 @@ function domainHasWhoisText(result) { export default async (req, res) => { const query = req.query.q; - if (!query) { + // typeof check also rejects the array form (?q=a&q=b) express produces. + if (!query || typeof query !== 'string') { return res.status(400).json({ error: 'No address provided' }); } if (!isValidIP(query) && !isValidDomain(query)) { diff --git a/api/sentry-tunnel.js b/api/sentry-tunnel.js index b301ebad..c84826de 100644 --- a/api/sentry-tunnel.js +++ b/api/sentry-tunnel.js @@ -13,6 +13,7 @@ import logger from '../common/logger.js'; // the DSN the SDK was configured with. Returns a URL or null. Exported for // tests. export const parseEnvelopeDsn = (rawBody) => { + if (!Buffer.isBuffer(rawBody)) return null; try { const text = rawBody.toString('utf8'); const newline = text.indexOf('\n'); @@ -43,6 +44,7 @@ const USER_ITEM_TYPES = new Set(['event', 'transaction', 'replay_event', 'feedba // parse trouble returns the body unchanged: losing an IP is acceptable, // losing the event is not. Exported for tests. export const injectUserIp = (rawBody, ip) => { + if (!Buffer.isBuffer(rawBody)) return rawBody; const NL = 0x0a; try { const headerEnd = rawBody.indexOf(NL); @@ -103,7 +105,15 @@ export default async (req, res) => { return res.status(404).json({ error: 'Tunnel not configured' }); } - const dsn = parseEnvelopeDsn(req.body); + // express.raw always yields a Buffer here, but guard anyway so a + // misconfigured parser (string / array body) can't reach the byte-level + // envelope walk below. + const rawBody = req.body; + if (!Buffer.isBuffer(rawBody)) { + return res.status(400).json({ error: 'Invalid envelope' }); + } + + const dsn = parseEnvelopeDsn(rawBody); if (!dsn) { return res.status(400).json({ error: 'Invalid envelope' }); } @@ -113,7 +123,7 @@ export default async (req, res) => { const visitorIp = visitorIpFrom(req.headers); - const body = visitorIp ? injectUserIp(req.body, visitorIp) : req.body; + const body = visitorIp ? injectUserIp(rawBody, visitorIp) : rawBody; try { const projectId = dsn.pathname.replace(/^\//, ''); diff --git a/common/rdap.js b/common/rdap.js index 9452be84..55b2c6fe 100644 --- a/common/rdap.js +++ b/common/rdap.js @@ -80,6 +80,7 @@ export async function rdapDomain(domain, { timeoutMs = 5000 } = {}) { // Numeric value of an IP for prefix math. Returns null on junk — callers // validate first, but bootstrap CIDR bases also pass through here. const ipToBigInt = (ip) => { + if (typeof ip !== 'string') return null; if (!ip.includes(':')) { const parts = ip.split('.'); if (parts.length !== 4) return null; diff --git a/frontend/components/User.vue b/frontend/components/User.vue index e0497c66..ffa93d2b 100644 --- a/frontend/components/User.vue +++ b/frontend/components/User.vue @@ -115,6 +115,13 @@ watch(() => store.allHasLoaded, (newVal) => { if (newVal) getUserInfo(); }); +// The unknown-auth-hint boot resolves sign-in AFTER allHasLoaded (background +// probe in main.js), so the watcher above already ran and skipped; retry when +// the signed-in state lands. getUserInfo self-guards against double fetches. +watch(isSignedIn, (signed) => { + if (signed && store.allHasLoaded) getUserInfo(); +}); + watch(() => triggerUserBenefits.value, (newVal) => { if (newVal) openUserBenefits(); }); diff --git a/frontend/firebase-init.js b/frontend/firebase-init.js index 11998374..7d85c39d 100644 --- a/frontend/firebase-init.js +++ b/frontend/firebase-init.js @@ -22,7 +22,12 @@ const loadFirebaseAuth = () => { ]).then(([{ initializeApp }, authModule]) => ({ ...authModule, auth: authModule.getAuth(initializeApp(firebaseConfig)), - })); + })).catch((error) => { + // Don't memoize a transient chunk-load failure — the next auth + // action should retry the import instead of replaying the rejection. + authModulePromise = null; + throw error; + }); return authModulePromise; }; diff --git a/tests/sentry-tunnel.test.js b/tests/sentry-tunnel.test.js index ac753ffb..d505dcd3 100644 --- a/tests/sentry-tunnel.test.js +++ b/tests/sentry-tunnel.test.js @@ -37,6 +37,18 @@ test('parseEnvelopeDsn returns null for garbage input', () => { assert.strictEqual(parseEnvelopeDsn(Buffer.from('{"no_dsn":1}\n{}')), null); }); +test('parseEnvelopeDsn returns null for non-Buffer input', () => { + assert.strictEqual(parseEnvelopeDsn(`{"dsn":"${OUR_DSN}"}\n{}`), null); + assert.strictEqual(parseEnvelopeDsn(undefined), null); +}); + +test('returns 400 when the body is not a Buffer', async () => { + process.env.VITE_SENTRY_DSN_FRONTEND = OUR_DSN; + const res = makeRes(); + await tunnelHandler({ method: 'POST', body: `{"dsn":"${OUR_DSN}"}\n{}` }, res); + assert.strictEqual(res.statusCode, 400); +}); + test('rejects non-POST methods with 405', async () => { const res = makeRes(); await tunnelHandler({ method: 'GET' }, res); @@ -146,6 +158,12 @@ test('injectUserIp leaves non-event items byte-identical', () => { assert.deepStrictEqual(JSON.parse(items[1][1]).user, { ip_address: IP }); }); +test('injectUserIp returns non-Buffer bodies unchanged', () => { + const str = '{"dsn":"x"}\n{"type":"event"}\n{}'; + assert.strictEqual(injectUserIp(str, IP), str); + assert.strictEqual(injectUserIp(undefined, IP), undefined); +}); + test('injectUserIp returns malformed bodies unchanged', () => { const headerOnly = Buffer.from('{"dsn":"x"}'); assert.strictEqual(injectUserIp(headerOnly, IP), headerOnly);