Sign-up guard: honeypot field renamed so browser autofill cannot trip it; log every guard rejection; clearer stale-page message
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -159,7 +159,7 @@ window.IAP = (function () {
|
||||
let pick = null;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const r = await (await fetch('/api/auth/email/start', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, fts: FORM_TS, website: (o.honeypot && o.honeypot.value) || '', pick }) })).json();
|
||||
body: JSON.stringify({ email, fts: FORM_TS, hp_field_x9: (o.honeypot && o.honeypot.value) || '', pick }) })).json();
|
||||
if (r.challenge && o.host) { pick = await iconCheck(o.host, r.challenge, r.error); continue; }
|
||||
if (r.error) throw new Error(r.error);
|
||||
return r;
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@
|
||||
<h3 id="jnCapH">Join free</h3>
|
||||
<p class="muted small" style="margin:0 0 12px" id="jnCapSub">Type your email and we send a 6-digit code. No password, no wallet needed today.</p>
|
||||
<p><input id="jnEmail" type="email" placeholder="Your email" autocomplete="email"></p>
|
||||
<input id="jnWebsite" class="hp-field" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<input id="jnWebsite" class="hp-field" type="text" name="hp_field_x9" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<div id="jnCheck" hidden></div>
|
||||
<p id="jnCodeRow" hidden><input id="jnCode" inputmode="numeric" placeholder="6-digit code from your inbox"></p>
|
||||
<p id="jnUnder" class="small muted" hidden style="margin:-4px 0 12px"></p>
|
||||
@@ -155,7 +155,7 @@
|
||||
InstantAdPay · <a href="/contract">Contract</a> · <a href="/terms">Terms</a> · <a href="/privacy">Privacy</a> · <a href="/disclaimer">Disclaimer</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260911b"></script>
|
||||
<script src="/assets/common.js?v=20260911c"></script>
|
||||
<script src="/assets/join.js?v=20260911a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@
|
||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees.</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260911b"></script>
|
||||
<script src="/assets/common.js?v=20260911c"></script>
|
||||
<script src="/assets/launch.js?v=20260911b"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@
|
||||
<p class="muted small">Type your email and we send a 6-digit code. No password to invent,
|
||||
no password to forget. New emails get a free account automatically.</p>
|
||||
<p><input id="mcEmail" type="email" placeholder="Email" autocomplete="email" style="width:100%"></p>
|
||||
<input id="mcWebsite" class="hp-field" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<input id="mcWebsite" class="hp-field" type="text" name="hp_field_x9" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<div id="mcCheck" hidden></div>
|
||||
<p id="mcCodeRow" hidden><input id="mcCode" inputmode="numeric" placeholder="6-digit code" style="width:100%"></p>
|
||||
<button class="btn" id="mcSendBtn">Email me a code</button>
|
||||
@@ -899,7 +899,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260911b"></script>
|
||||
<script src="/assets/common.js?v=20260911c"></script>
|
||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||
<script src="/assets/promo.js?v=20260911a"></script>
|
||||
<script src="/assets/my.js?v=20260911v"></script>
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@
|
||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260910c"></script>
|
||||
<script src="/assets/common.js?v=20260911c"></script>
|
||||
<script src="/assets/plays.js?v=20260910b"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@
|
||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never share your recovery phrase.</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260910c"></script>
|
||||
<script src="/assets/common.js?v=20260911c"></script>
|
||||
<script src="/assets/wallets.js?v=20260911b"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -133,14 +133,18 @@ function codeChallenge(rec) {
|
||||
return { prompt: pick[answer][1], options: pick.map(x => x[0]) };
|
||||
}
|
||||
// returns null to allow the send, or { status, body } to answer with instead
|
||||
const guardLog = (req, why, b) => console.log('signup-guard', why, clientIp(req), String(b.email || '').replace(/^(.).*(@.*)$/, '$1***$2'));
|
||||
function codeGuard(req, b) {
|
||||
const now = Date.now();
|
||||
if (b.website) return { status: 200, body: { ok: true, sent: true } }; // honeypot: bots fill it, humans never see it
|
||||
// honeypot: bots fill it, humans never see it. The field carries a nonsense name so browser
|
||||
// autofill (which likes "website" and "url") cannot fill it for a real person.
|
||||
if (b.hp_field_x9) { guardLog(req, 'honeypot', b); return { status: 200, body: { ok: true, sent: true } }; }
|
||||
const fts = Number(b.fts) || 0;
|
||||
if (!fts || now - fts < CODE_LIMITS.minFormMs || now - fts > 12 * 3600 * 1000) return { status: 400, body: { error: 'Give the page a second, then tap again.' } };
|
||||
if (!fts || now - fts < CODE_LIMITS.minFormMs) { guardLog(req, 'form-age', b); return { status: 400, body: { error: 'Give the page a second, then tap again.' } }; }
|
||||
if (now - fts > 12 * 3600 * 1000) { guardLog(req, 'form-stale', b); return { status: 400, body: { error: 'This page has been open a long time. Refresh it, then tap again.' } }; }
|
||||
const minute = Math.floor(now / 60000);
|
||||
if (codeGlobal.minute !== minute) { codeGlobal.minute = minute; codeGlobal.n = 0; }
|
||||
if (codeGlobal.n >= CODE_LIMITS.globalPerMin) { codeTrip(req, 'global'); return { status: 429, body: { error: 'Busy right now. Try again in a minute.' } }; }
|
||||
if (codeGlobal.n >= CODE_LIMITS.globalPerMin) { guardLog(req, 'global-limit', b); codeTrip(req, 'global'); return { status: 429, body: { error: 'Busy right now. Try again in a minute.' } }; }
|
||||
const ip = clientIp(req);
|
||||
const rec = codeHits.get(ip) || { t: [], passUntil: 0, chal: null };
|
||||
rec.t = rec.t.filter(ts => now - ts < 24 * 3600 * 1000);
|
||||
@@ -150,7 +154,7 @@ function codeGuard(req, b) {
|
||||
const pick = String(b.pick || '');
|
||||
if (pick && rec.chal && rec.chal.exp > now && pick === rec.chal.answer) { rec.passUntil = now + CODE_LIMITS.passMs; rec.chal = null; }
|
||||
else {
|
||||
codeTrip(req, ip);
|
||||
guardLog(req, pick ? 'wrong-pick' : 'ip-limit', b); codeTrip(req, ip);
|
||||
const challenge = codeChallenge(rec); codeHits.set(ip, rec);
|
||||
return { status: 429, body: { error: pick ? 'That was not it. Try once more.' : 'Quick check before we send another code.', challenge } };
|
||||
}
|
||||
@@ -722,7 +726,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const e = String(b.email || '').trim().toLowerCase();
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)) return json(res, 400, { error: 'That email address does not look right.' });
|
||||
const prev = emailCodes.get(e);
|
||||
if (prev && Date.now() < prev.nextAt) return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' });
|
||||
if (prev && Date.now() < prev.nextAt) { console.log('signup-guard cooldown', clientIp(req), e.replace(/^(.).*(@.*)$/, '$1***$2')); return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' }); }
|
||||
const guard = codeGuard(req, b); // honeypot, form age, per-IP + global limits, icon check once limited
|
||||
if (guard) return json(res, guard.status, guard.body);
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||
|
||||
Reference in New Issue
Block a user