| # | Title | Severity | Category | File / Layer |
|---|---|---|---|---|
| F-01 | Plaintext mnemonic & WIF stored in localStorage | CRITICAL | Wallet Security | app.html |
| F-02 | Hardcoded admin-reset secret in source code | CRITICAL | API Security | vitocoin/api.py |
| F-03 | X-Forwarded-For trusted unconditionally (IP spoofing) | HIGH | API Security | vitocoin/api.py |
| F-04 | 51% / Majority-hashrate attack (3-node centralised mining) | HIGH | Consensus Security | Architecture |
| F-05 | Merchant mnemonic hardcoded in node.py | HIGH | Wallet Security | node.py |
| F-06 | No Cross-Site Request Forgery (CSRF) protection on POST /tx | HIGH | API Security | vitocoin/api.py |
| F-07 | Mempool double-spend in same block (within-block tx ordering) | MEDIUM | Transaction Security | vitocoin/blockchain.py |
| F-08 | Sybil / Eclipse attack — only 3 known seed nodes | MEDIUM | Node / Consensus | vitocoin/network.py |
| F-09 | Fake hashrate self-reporting bypasses 85% ban threshold | MEDIUM | Mining Security | vitocoin/api.py |
| F-10 | PBKDF2 iterations too low (2048) for mnemonic KDF | MEDIUM | Wallet Security | vitocoin/crypto.py / app.html |
| F-11 | No Content Security Policy (CSP) header on API responses | MEDIUM | API Security | vitocoin/api.py |
| F-12 | Genesis block PoW not enforced (accepts nonce=0) | LOW | Consensus Security | vitocoin/blockchain.py |
| F-13 | Orphan pool LRU eviction based on count, not weight | LOW | Node Security | vitocoin/blockchain.py |
| F-14 | No replay protection across testnet / mainnet chains | LOW | Transaction Security | vitocoin/transaction.py |
| F-15 | Wallet export contains plaintext seed JSON | LOW | Wallet Security | app.html |
The frontend persists wallet objects directly to localStorage using
JSON.stringify(_wallets). Each wallet object contains raw fields
mnemonic and wif in plaintext. Despite the UI claiming
"your private keys are encrypted locally", no AES/GCM or PBKDF2 key-wrapping is ever
applied before the write. Any XSS payload, browser extension, or physical access to DevTools can
extract all private keys instantly.
// app.html ~line 2815 — mnemonic saved in cleartext
_wallets.push({address:f.address, mnemonic, wif:f.derived.wif, encrypted_key:f.derived.wif, ...});
saveWallets(); // ← calls localStorage.setItem('vc_wallets', JSON.stringify(_wallets))
localStorage.getItem('vc_wallets') and exfiltrates all mnemonics and WIF keys to an attacker server.mnemonic field from stored wallet objects entirely — re-derive keys at runtime from the encrypted seed.CryptoKey with extractable:false for in-memory signing keys.
The merchant engine is seeded from a mnemonic phrase that is hardcoded as a string literal in
node.py. Anyone with read access to the file (all three VPS root accounts, the Git
repository history) can derive every payment address and their private keys for any index.
_merchant_mnemonic = 'cinnamon between manual satisfy off child deputy violin crowd shuffle never wrist' _me = create_merchant_engine(chain.utxo, lambda: chain.height) register_merchant_from_mnemonic(_me, 'vitocoin-merchant', _merchant_mnemonic, ...)
VITO_MERCHANT_MNEMONIC) or a secrets manager (HashiCorp Vault, AWS Secrets Manager).node.py to .gitignore secrets scan or use git-secrets pre-commit hook.
Both the Python and JavaScript implementations use PBKDF2-HMAC-SHA512 with only
2048 iterations for mnemonic-to-seed derivation. The BIP-39 reference spec recommends 2048 but
this predates modern GPU cracking. NIST SP 800-132 recommends ≥310,000 iterations for
SHA-256 and modern hardware can perform ~1 billion SHA-512 PBKDF2 iterations/second on a GPU,
making brute-force of weak passphrases practical.
os.urandom) — only affects passphrase-protected wallets.
The export function serialises _wallets (which includes raw mnemonics and WIF keys) to a
JSON file and triggers a download with no encryption or password protection.
os.urandom (CSPRNG) — no weak/deterministic RNG[1, N-1] on construction0x9e with Base58Check checksumhmac.compare_digest used for Base58Check checksum comparison (constant-time)__repr__ returns [HIDDEN]
_validate_block iterates over non-coinbase transactions and validates each against the
current UTXO set without tracking which inputs are consumed by earlier transactions in the same
block. If two transactions in the same block spend the same UTXO, the first will pass validation and
remove the UTXO from the set, while the second transaction will correctly fail with "UTXO not found".
However, a malicious miner constructing a custom block could craft a scenario where this
ordering-dependent validation creates an inconsistency.
# blockchain.py _validate_block — UTXOs not tracked within-block
for tx in block.transactions[1:]:
for inp in tx.inputs:
utxo_out = self.utxo.get(inp.prev_txid, inp.prev_index)
if utxo_out is None:
return False, f"UTXO not found ..."
# ← no set to track "already spent in this block"
apply_block processes transactions sequentially and pops UTXOs, so the second tx fails — but there is no explicit within-block spent-set check to make this a hard guarantee.spent_in_block = set() during _validate_block and reject any tx that references an input already in that set.CCoinsViewCache spent-set pattern.The transaction signature preimage (sighash) does not include any chain-specific identifier. A signed mainnet transaction is structurally valid on testnet and vice versa. If a future hard fork is introduced without replay protection, transactions could be replayed across chains.
magic bytes or a chain-specific SIGHASH fork flag in the signature preimage (BIP-341 approach).validate_syntax() — same (txid, index) cannot appear twicecb_out ≤ subsidy + fees — cannot inflate supply
A chain-wipe endpoint exists that deletes all SQLite database files and restarts the node. The
authenticating secret is a hardcoded string literal 'VitoReset2026Secure' in the
source code. Anyone who reads the source code can wipe the entire blockchain from any node.
Additionally, it is unclear how this function is registered as a URL route — if reachable, it
represents a critical destructive capability with minimal protection.
def _handle_admin_reset(self, secret):
RESET_SECRET = 'VitoReset2026Secure' # ← hardcoded in source
if secret != RESET_SECRET:
return {'error': 'Unauthorized'}, 403
# Wipes chainstore.db, chainstore.db-shm, chainstore.db-wal
# Then: subprocess.run(['systemctl', 'restart', 'vitocoin.service'])
subprocess.run(['systemctl', 'restart', ...]) call creates a system-level execution path from an HTTP handler.os.environ['VITO_RESET_SECRET'] — never hardcode.127.0.0.1 (restrict via nginx allow 127.0.0.1; deny all;).subprocess.run with a proper signal/IPC mechanism to avoid shell-injection risk.
Both the module-level _rate_limit() function and the class-level
RateLimiter derive the client IP from the raw
X-Forwarded-For header without validating that the request originates from a trusted
proxy. An attacker can set X-Forwarded-For: 1.2.3.4 on every request to appear as an
arbitrary IP and completely bypass per-IP rate limits.
def _get_client_ip(handler):
xff = handler.headers.get("X-Forwarded-For", "")
if xff:
return xff.split(",")[0].strip() # ← no trusted-proxy check
return handler.client_address[0]
handler.client_address[0] matches the known nginx proxy IP (e.g. 127.0.0.1).TRUSTED_PROXIES list and validate before using XFF value.proxy_set_header X-Real-IP $remote_addr and use that instead.
The POST /tx endpoint accepts JSON with Access-Control-Allow-Origin: *.
While signed transactions protect funds from fabrication, a malicious page could use
fetch() to trick a user's browser into broadcasting a pre-constructed transaction
that the user previously signed and the attacker captured. Combined with the wallet's session state
stored in localStorage (accessible from the same origin), an XSS attack can directly broadcast
transactions.
Access-Control-Allow-Origin: * with an explicit allowlist of trusted origins.X-VITO-Token) checked server-side.
The API sends X-Frame-Options, X-Content-Type-Options, and
Referrer-Policy but omits Content-Security-Policy.
The Vercel-hosted frontend also lacks a CSP (confirmed in infrastructure audit).
Without CSP, XSS payloads can exfiltrate localStorage wallet data to arbitrary external servers.
Content-Security-Policy: default-src 'self'; connect-src 'self' https://vitocoin.com; script-src 'self' 'unsafe-inline' (then progressively remove unsafe-inline by externalising scripts).Strict-Transport-Security: max-age=63072000; includeSubDomains to the nginx config._HEX64_RE, _ADDR_RE) — no injection vectors_int_param(lo, hi) — no integer overflow / path traversalos.path.realpath prefix check/peers/connect: private IP ranges blocked via _PRIVATE_NETS listMAX_BODY) — no memory exhaustion from large bodieshmac.compare_digest (constant-time, no timing oracle)The miner registration function attempts to auto-ban IPs that claim >85% of total network hashrate. However, the threshold is computed against self-reported hashrate values submitted by miners themselves via the API. A malicious miner can report a tiny hashrate (e.g., 1 H/s) while actually submitting valid blocks. Conversely, a miner could report an inflated hashrate for competing IPs to trigger false bans.
# api.py _register_miner — ban check uses attacker-controlled value
network_hr = sum(v["hashrate_hps"] for v in _MINER_REGISTRY.values())
if network_hr > 0 and hashrate_hps / network_hr > 0.85 and hashrate_hps > 5_000_000:
_MINER_BAN_LIST.add(ip) # ← hashrate_hps is self-reported by the miner
_validate_header)subsidy + fees_next_bits() — cannot submit blocks with artificially easy difficultyGET /mining/template — browser miners can participate without full node
All three VPS mining nodes use an identical --wallet address (the admin wallet) and
collectively control 100% of the current network hashrate (Node 1: 8 threads, Nodes 2&3: 4 threads
each). This means a single compromised admin account controls the entire blockchain's mining output.
An attacker who gains access to the admin credentials can execute a chain reorganisation, double-spend,
or transaction censorship with absolute certainty.
/join-network guide aggressively to bootstrap external miners.
The network currently has only 3 nodes, all operated by the same entity. SEED_NODES are
the three VPS IPs; DNS seeds (seed1.vitocoin.net, seed2.vitocoin.net) do not
resolve. With so few peers, eclipse attacks (surrounding a node with adversarial connections) are
trivially achievable: an attacker running 4 nodes can disconnect a victim node from the honest chain.
MAX_OUTBOUND=8, all outbound slots can be filled by adversarial nodes.
The genesis block is created with nonce=0 and its hash almost certainly does not satisfy
the stated GENESIS_BITS=0x1d00ffff target. The comment reads "For testnet/dev: accept
genesis without valid PoW." This sets a precedent that the genesis block is a special case exempt from
PoW, which is fine by convention — but the code comment could lead future developers to create dev
environments with consistently invalid genesis blocks.
The orphan pool is capped at 5,000 blocks by count. A 4 MB block at maximum size means 5,000 orphans could consume up to 20 GB of RAM on a node, causing OOM. Orphan blocks should be limited by total memory weight, not count.
MAX_ORPHANS count check with a MAX_ORPHAN_BYTES = 64 * 1024 * 1024 (64 MB) weight cap.BAN_THRESHOLD)MAX_REORG_DEPTH) — prevents deep chain rewind DoS/peers/connect: private and reserved IP ranges blocked| Priority | Finding | Effort | Action |
|---|---|---|---|
| CRITICAL | F-01 Plaintext keys in localStorage | Medium | Encrypt wallet storage with AES-256-GCM + PBKDF2 before writing to localStorage |
| CRITICAL | F-02 Hardcoded reset secret | Low | Remove endpoint from production; move secret to env var if kept |
| HIGH | F-05 Hardcoded merchant mnemonic | Low | Move to env var; rotate mnemonic immediately |
| HIGH | F-03 XFF IP spoofing | Low | Only trust XFF from known proxy IP (127.0.0.1) |
| HIGH | F-06 No CSRF protection | Low | Restrict CORS origin; add CSRF token for sensitive endpoints |
| HIGH | F-04 51% attack risk | High | Promote public mining; separate wallet keys per node |
| MEDIUM | F-07 Within-block double-spend | Low | Add spent_in_block set to _validate_block() |
| MEDIUM | F-08 Eclipse / Sybil risk | Medium | Add DNS seeds; subnet diversity check on peer connections |
| MEDIUM | F-09 Fake hashrate reporting | Medium | Derive hashrate from valid block submissions, not self-report |
| MEDIUM | F-10 PBKDF2 low iterations | Low | Use ≥310k iterations for wallet-encryption KDF (separate from BIP-39) |
| MEDIUM | F-11 Missing CSP | Low | Add CSP header to Vercel config and nginx |