@webdecoy/fcaptcha 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.js +61 -3
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -171,6 +171,35 @@ const fingerprintStore = {
|
|
|
171
171
|
}
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
+
// Token Store - prevents token replay attacks
|
|
175
|
+
const tokenStore = {
|
|
176
|
+
usedTokens: new Set(),
|
|
177
|
+
|
|
178
|
+
// Mark a token as used (returns false if already used)
|
|
179
|
+
markUsed(tokenSig) {
|
|
180
|
+
if (this.usedTokens.has(tokenSig)) {
|
|
181
|
+
return false; // Already used
|
|
182
|
+
}
|
|
183
|
+
this.usedTokens.add(tokenSig);
|
|
184
|
+
|
|
185
|
+
// Cleanup old tokens periodically (tokens expire after 5 min anyway)
|
|
186
|
+
if (Math.random() < 0.1) this._cleanup();
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
isUsed(tokenSig) {
|
|
191
|
+
return this.usedTokens.has(tokenSig);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
_cleanup() {
|
|
195
|
+
// In production with Redis, use TTL instead
|
|
196
|
+
// For in-memory, just clear if too large (tokens expire in 5 min)
|
|
197
|
+
if (this.usedTokens.size > 50000) {
|
|
198
|
+
this.usedTokens.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
174
203
|
// =============================================================================
|
|
175
204
|
// Detection Patterns
|
|
176
205
|
// =============================================================================
|
|
@@ -629,7 +658,7 @@ function generateToken(ip, siteKey, score) {
|
|
|
629
658
|
return Buffer.from(JSON.stringify(data)).toString('base64url');
|
|
630
659
|
}
|
|
631
660
|
|
|
632
|
-
function verifyToken(token) {
|
|
661
|
+
function verifyToken(token, ip = null) {
|
|
633
662
|
try {
|
|
634
663
|
const decoded = JSON.parse(Buffer.from(token, 'base64url').toString());
|
|
635
664
|
|
|
@@ -648,11 +677,28 @@ function verifyToken(token) {
|
|
|
648
677
|
return { valid: false, reason: 'invalid_signature' };
|
|
649
678
|
}
|
|
650
679
|
|
|
680
|
+
// Check for token replay (single-use tokens)
|
|
681
|
+
if (tokenStore.isUsed(sig)) {
|
|
682
|
+
return { valid: false, reason: 'token_already_used' };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Verify IP matches (if provided)
|
|
686
|
+
if (ip) {
|
|
687
|
+
const expectedIpHash = crypto.createHash('sha256').update(ip).digest('hex').slice(0, 8);
|
|
688
|
+
if (decoded.ip_hash !== expectedIpHash) {
|
|
689
|
+
return { valid: false, reason: 'ip_mismatch' };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Mark token as used (prevents replay)
|
|
694
|
+
tokenStore.markUsed(sig);
|
|
695
|
+
|
|
651
696
|
return {
|
|
652
697
|
valid: true,
|
|
653
698
|
site_key: decoded.site_key,
|
|
654
699
|
timestamp: decoded.timestamp,
|
|
655
|
-
score: decoded.score
|
|
700
|
+
score: decoded.score,
|
|
701
|
+
ip_hash: decoded.ip_hash
|
|
656
702
|
};
|
|
657
703
|
} catch (e) {
|
|
658
704
|
return { valid: false, reason: e.message };
|
|
@@ -816,7 +862,19 @@ app.post('/api/score', (req, res) => {
|
|
|
816
862
|
|
|
817
863
|
app.post('/api/token/verify', (req, res) => {
|
|
818
864
|
const { token } = req.body;
|
|
819
|
-
|
|
865
|
+
|
|
866
|
+
// Extract client IP for verification
|
|
867
|
+
let ip = req.headers['x-real-ip'] || '';
|
|
868
|
+
if (!ip) {
|
|
869
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
870
|
+
if (forwarded) {
|
|
871
|
+
ip = forwarded.split(',')[0].trim();
|
|
872
|
+
} else {
|
|
873
|
+
ip = req.socket.remoteAddress;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
res.json(verifyToken(token, ip));
|
|
820
878
|
});
|
|
821
879
|
|
|
822
880
|
// PoW Challenge endpoint - client fetches this on page load
|