Version audited:
0.0.2(2026-08-29)
Auditor: Internal review (triggered by AI-assistant flag: “0.0.2 çok yeni — 126 indirme, zero-deps avantaj ama audit edilmeli”)
Scope:src/captcha.ts,src/png.ts,src/jpeg.ts,src/svg.ts,src/font.ts,package.json,dist/output,npm audit
Verdict: ✅ Safe to use with recommended hardening — no backdoor, no obfuscation, zero-deps claim verified. One low-severity TTL issue noted and mitigated.
capcode stateless CAPTCHA üretimi ve doğrulaması sunar: rastgele kod → PNG/SVG/JPEG → HMAC-SHA256 ile imzalı opak token (nonce.ts.tag) → timingSafeEqual ile doğrulama. Sadece Node.js built-in’leri (node:crypto, node:zlib) kullanılır, native bağımlılık yoktur.
| Claim | Result |
|---|---|
| Zero dependencies | ✅ Verified — package.json:49 only devDependencies |
| No canvas / no native build | ✅ Verified — pure TS encoders src/png.ts:1, src/jpeg.ts:1, src/svg.ts:1 |
| Stateless HMAC verification | ✅ Verified — src/captcha.ts:327 / 347 |
| Very new / low download count |
npm audit 0 vulnerabilities, npm test 18/18 pass.
| Threat | Mitigation in capcode | Residual Responsibility (Integrator) |
|---|---|---|
| Token forgery without secret | HMAC-SHA256 over nonce:ts:code with 16-byte random nonce src/captcha.ts:332 |
Keep secret server-side only, ≥32 bytes, rotated periodically |
| Token replay / infinite lifetime | maxAge TTL check src/captcha.ts:378 |
Always pass maxAge at verifyCode time (e.g. 300s) |
| Timing side-channel | timingSafeEqual src/captcha.ts:392 |
None |
| OCR / segmentation | Per-char rotation/shear/scale jitter, overlap, wavy noise src/captcha.ts:652-706 |
Choose difficulty: "hard" for high-value flows |
| Brute-force | Not in scope (stateless) | Add rate-limit + IP throttling + single-use token cache |
| Supply-chain | 0 deps, no postinstall, minimal files package.json:8 |
Pin 0.0.2 exactly, verify npm provenance on update |
Out of scope: DDoS, client-side bypass (user can always request new image), accessibility.
Location: src/captcha.ts:322-336 (hashCode), src/captcha.ts:347-393 (verifyCode)
v2: nonce.ts.tag nonce=32 hex (16 random bytes), ts=base36 unix seconds, tag=HMAC(secret, "nonce:ts:NORMALIZED_CODE")
v1: nonce.tag legacy, rejected when maxAge set
normalizeCode()src/captcha.ts:192strips whitespace and uppercases — prevents trivial bypass (" a b 3x "→"AB3X").randomBytes(16)is CSPRNG, prevents precomputation.v2always embeds timestamp;v1intentionally rejected withmaxAgesrc/captcha.ts:369→ forces TTL.- Regex validation
src/captcha.ts:374-375strictly allows hex/base36.
Severity: None — design is sound and follows stateless signed-token best practice (similar to JWT HMAC).
secretrequired, throws if missingsrc/captcha.ts:351.- Recomputes HMAC with caller-supplied
codethentimingSafeEqualsrc/captcha.ts:386-392with length check — constant-time. - Malformed
id.split(".")length ≠2/3 →falsesrc/captcha.ts:371.
Low-severity issue — Future timestamp not rejected:
// src/captcha.ts:378-381
if (maxAge !== undefined && ts !== null) {
const issuedAtSeconds = parseInt(ts, 36);
const nowSeconds = Math.floor(Date.now() / 1000);
if (nowSeconds - issuedAtSeconds > maxAge) return false;
}If an attacker forges ts = now + 3600, then now - ts = -3600, condition is false, token passes even with maxAge: 300. Live-verified:
const future = nonce + "." + futureTs + "." + hmac(nonce+":"+futureTs+":ABCD");
verifyCode(future, "ABCD", { secret, maxAge: 300 }) // → true (should be false)Recommendation: Reject future tokens beyond clock skew (e.g. 60s):
if (issuedAtSeconds > nowSeconds + 60) return false;Severity: Low — requires attacker to know secret to forge HMAC anyway; but with leaked secret or weak secret, extends window. Fix in next patch; integrator can add wrapper check meanwhile.
length 1–64,scale 1–16enforcedRangeErrorsrc/captcha.ts:269-270.charset≥2 charssrc/captcha.ts:271,glyphsvalidated against charsetsrc/captcha.ts:282-288.themeRGB0–255src/captcha.ts:256.noise.jitter 0–3,opacity 0–1src/captcha.ts:218-228.
- Only
node:crypto(createHmac,timingSafeEqual,randomInt,randomBytes)src/captcha.ts:1— no custom crypto. - Default
sha256,sha512opt-insrc/captcha.ts:267. No downgrade via token (algorithm not embedded in token — server decides). dist/contains noeval,child_process,fetch(verified by scan).
| Format | File | Verdict |
|---|---|---|
| PNG | src/png.ts:32 — manual IHDR/IDAT/IEND + deflateSync + CRC32 src/png.ts:3-13 |
✅ Valid PNG signature 89 50 4E 47, deterministic |
| JPEG | src/jpeg.ts:288 — Baseline DCT, JFIF 1.01, 4:4:4, standard quant/Huffman tables |
✅ Valid SOI FF D8 / EOI FF D9, quality 85 default |
| SVG | src/svg.ts:9 — dominant-color rect + grouped path per color |
✅ <svg output, size-optimized via row coalescing |
No native canvas/sharp dependency — confirmed advantage over captcha-canvas / trek-captcha.
package.json:49dev-only,files: ["dist","README.md"]minimal publish, nopostinstall/preinstall.npm audit: 0 vulnerabilities,tsconfig.json:7strict: true.- Repository
git logclean (5 commits), no minified obfuscation insrc/.
import { createCaptcha, verifyCode } from "capcode";
// 1. Pin exact version in package.json
// "capcode": "0.0.2" — avoid ^
// 2. Strong secret, env only
const SECRET = process.env.CAPTCHA_SECRET; // ≥32 random bytes, e.g. `openssl rand -hex 32`
if (!SECRET || SECRET.length < 32) throw new Error("weak secret");
// 3. Always enforce TTL at verify time (not create time)
const captcha = createCaptcha({ secret: SECRET, difficulty: "hard", format: "png" });
// send captcha.id + captcha.image.dataUrl to client
const ok = verifyCode(captcha.id, userInput, {
secret: SECRET,
algorithm: "sha256", // lock algorithm
maxAge: 300, // 5 minutes
});
// 4. Single-use + rate-limit (outside library)
if (ok) {
if (usedTokens.has(captcha.id)) return false; // replay protection
usedTokens.add(captcha.id);
}
// 5. Optional wrapper for future-timestamp fix until upstream patch
function safeVerify(id, code, opts) {
const parts = id.split(".");
if (parts.length === 3) {
const ts = parseInt(parts[1], 36);
if (ts > Math.floor(Date.now()/1000) + 60) return false;
}
return verifyCode(id, code, opts);
}Additional:
- Use
difficulty: "hard"for login/payment,mediumfor low-risk forms. - Set
charsetwithout ambiguous chars (defaultABCDEFGHJKLMNPQRSTUVWXYZ23456789already excludesI/O/0/1). - Log
verifyCodefailures and throttle IP after N attempts.
- Reporting: Open an issue at
https://github.com/cihatksm/capcode/issuesor contact maintainercihatksm. Do not disclose unpatched high-severity issues publicly. - Update policy: Review
CHANGELOG/Releasesbefore bumping version; runnpm auditandnpm testin CI. - Next audit due: On next minor bump or after TTL fix is merged — re-verify
src/captcha.ts:378.
- Source reviewed:
src/captcha.ts:1-767,src/png.ts,src/jpeg.ts:1-396,src/svg.ts:1-78,src/font.ts - Tests:
test/captcha.test.mjs:1-153(18 tests),benchmark.mjs,generate.mjs - Standards: HMAC (RFC 2104), PNG (RFC 2083), JPEG JFIF 1.01, SVG 1.1
This report can be included in README.md under “Security” or kept as standalone SECURITY.md per GitHub convention.