⬡ VITOCOIN SECURITY AUDIT REPORT CONFIDENTIAL

Auditor: Blockchain Security Auditor  |  Scope: Full-stack (node backend, P2P network, REST API, frontend wallet)  |  Date: April 9 2026  |  Chain version: VitoCoin Core v2.0  |  Codebase: /opt/vitocoin/vitocoin/ on 3× VPS nodes + app.html frontend
2
Critical
4
High
5
Medium
4
Low / Info
15
Passed checks
Findings Summary
#TitleSeverityCategoryFile / Layer
F-01Plaintext mnemonic & WIF stored in localStorageCRITICALWallet Securityapp.html
F-02Hardcoded admin-reset secret in source codeCRITICALAPI Securityvitocoin/api.py
F-03X-Forwarded-For trusted unconditionally (IP spoofing)HIGHAPI Securityvitocoin/api.py
F-0451% / Majority-hashrate attack (3-node centralised mining)HIGHConsensus SecurityArchitecture
F-05Merchant mnemonic hardcoded in node.pyHIGHWallet Securitynode.py
F-06No Cross-Site Request Forgery (CSRF) protection on POST /txHIGHAPI Securityvitocoin/api.py
F-07Mempool double-spend in same block (within-block tx ordering)MEDIUMTransaction Securityvitocoin/blockchain.py
F-08Sybil / Eclipse attack — only 3 known seed nodesMEDIUMNode / Consensusvitocoin/network.py
F-09Fake hashrate self-reporting bypasses 85% ban thresholdMEDIUMMining Securityvitocoin/api.py
F-10PBKDF2 iterations too low (2048) for mnemonic KDFMEDIUMWallet Securityvitocoin/crypto.py / app.html
F-11No Content Security Policy (CSP) header on API responsesMEDIUMAPI Securityvitocoin/api.py
F-12Genesis block PoW not enforced (accepts nonce=0)LOWConsensus Securityvitocoin/blockchain.py
F-13Orphan pool LRU eviction based on count, not weightLOWNode Securityvitocoin/blockchain.py
F-14No replay protection across testnet / mainnet chainsLOWTransaction Securityvitocoin/transaction.py
F-15Wallet export contains plaintext seed JSONLOWWallet Securityapp.html
1. Wallet Security
CRITICAL
F-01 — Plaintext mnemonic & WIF stored in localStorage
app.html : line 1853, 2604, 2810, 2815, 2880

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))

Attack Scenario

  • Any XSS (even reflected) runs localStorage.getItem('vc_wallets') and exfiltrates all mnemonics and WIF keys to an attacker server.
  • A malicious browser extension with "read all site data" permission drains all wallets silently.
  • Shared device / public computer → keys left in storage after session.

Recommended Fix

  • Wrap private key material with AES-256-GCM derived from a user password via PBKDF2 (≥310,000 iterations, random 16-byte salt) before writing to localStorage.
  • Remove mnemonic field from stored wallet objects entirely — re-derive keys at runtime from the encrypted seed.
  • Add a session timeout that clears the decrypted key from memory.
  • Consider using the Web Crypto API CryptoKey with extractable:false for in-memory signing keys.
HIGH
F-05 — Merchant mnemonic hardcoded in node.py
node.py : line ~113

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, ...)

Attack Scenario

  • Any developer or sysadmin with repo / server access can derive all merchant receiving addresses and spend any incoming merchant payments.
  • If the source repo is ever leaked, merchant funds are permanently at risk.

Recommended Fix

  • Load mnemonic from an environment variable (VITO_MERCHANT_MNEMONIC) or a secrets manager (HashiCorp Vault, AWS Secrets Manager).
  • Rotate the merchant mnemonic immediately — sweep funds to a new wallet.
  • Add node.py to .gitignore secrets scan or use git-secrets pre-commit hook.
MEDIUM
F-10 — PBKDF2 iteration count too low (2048)
vitocoin/crypto.py : line 448  |  app.html : line 2676

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.

Impact

  • BIP-39 allows an optional passphrase; if users set one, low iterations makes it brute-forceable with a GPU rig within hours for common passwords.
  • No impact on the mnemonic itself (entropy is from os.urandom) — only affects passphrase-protected wallets.

Recommended Fix

  • For wallet-encryption KDF (F-01 fix), use ≥310,000 PBKDF2-SHA256 or Argon2id.
  • The mnemonic-to-seed path (BIP-39) is fixed at 2048 by the standard and cannot be changed without breaking wallet compatibility — document this and advise strong passphrases.
LOW
F-15 — Wallet export downloads plaintext seed JSON
app.html : line 3654

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.

Recommended Fix

  • Encrypt the export file with AES-256-GCM under a user-chosen password before download.
  • Warn users explicitly that the file contains all private keys and must be stored securely.
Wallet Security — Passed Checks
  • secp256k1 keys generated via os.urandom (CSPRNG) — no weak/deterministic RNG
  • Full BIP-39 2048-word English wordlist — correct entropy-to-mnemonic encoding (bit-extraction fixed)
  • BIP-32 / BIP-44 HD derivation with hardened account and coin-type steps
  • PrivateKey scalar validated in range [1, N-1] on construction
  • WIF uses correct VitoCoin version byte 0x9e with Base58Check checksum
  • hmac.compare_digest used for Base58Check checksum comparison (constant-time)
  • PrivateKey raw bytes never logged or serialised; __repr__ returns [HIDDEN]
2. Transaction Security
MEDIUM
F-07 — Within-block double-spend not explicitly checked
vitocoin/blockchain.py : _validate_block() ~line 587

_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"

Attack Scenario

  • A miner controlling block construction could theoretically order two conflicting transactions so the validation loop processes a UTXO twice before the UTXO set is updated, depending on block-apply order.
  • In practice 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.

Recommended Fix

  • Maintain a spent_in_block = set() during _validate_block and reject any tx that references an input already in that set.
  • This mirrors Bitcoin Core's CCoinsViewCache spent-set pattern.
LOW
F-14 — No cross-chain replay protection (mainnet / testnet)
vitocoin/transaction.py : serialize(), SIGHASH_ALL

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.

Recommended Fix

  • Include the chain magic bytes or a chain-specific SIGHASH fork flag in the signature preimage (BIP-341 approach).
  • At minimum, add a version field to distinguish mainnet vs testnet transactions.
Transaction Security — Passed Checks
  • UTXO-based model — no account balance, no replay by design for confirmed UTXOs
  • ECDSA signature verified on every non-coinbase input before block acceptance
  • Duplicate-input check in validate_syntax() — same (txid, index) cannot appear twice
  • Coinbase maturity enforced (100 blocks) in both mempool and block validation
  • Dust limit (546 sat) enforced — no economically unspendable micro-outputs
  • MAX_TX_SIZE (100 KB) and MAX_BLOCK_SIZE (4 MB) enforced
  • Coinbase reward validated: cb_out ≤ subsidy + fees — cannot inflate supply
  • Merkle root recomputed and verified against block header
3. API Security
CRITICAL
F-02 — Hardcoded admin chain-reset secret in source code
vitocoin/api.py : _handle_admin_reset() line ~1263

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'])

Attack Scenario

  • Attacker reads source code (insider, leaked repo) → calls the reset endpoint → entire blockchain on that node is wiped and service restarted.
  • Even if the route is unreachable today, it will be exposed if the API is ever deployed without the nginx proxy.
  • The subprocess.run(['systemctl', 'restart', ...]) call creates a system-level execution path from an HTTP handler.

Recommended Fix

  • Immediate: Remove or disable this endpoint from production code.
  • Load the secret from os.environ['VITO_RESET_SECRET'] — never hardcode.
  • Require the endpoint to only accept connections from 127.0.0.1 (restrict via nginx allow 127.0.0.1; deny all;).
  • Replace subprocess.run with a proper signal/IPC mechanism to avoid shell-injection risk.
HIGH
F-03 — X-Forwarded-For trusted unconditionally (rate-limit bypass)
vitocoin/api.py : _get_client_ip() line 43  |  api.py module-level _rate_limit()

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]

Attack Scenario

  • Attacker sends 10,000 POST /tx requests with rotating spoofed XFF IPs → bypasses the 5/min tx rate limit → mempool flood / DoS.
  • Fail2ban sees only the nginx proxy IP in access logs (real source), but internal RateLimiter sees spoofed IPs → mismatch in ban effectiveness.

Recommended Fix

  • Only trust XFF when handler.client_address[0] matches the known nginx proxy IP (e.g. 127.0.0.1).
  • Set a TRUSTED_PROXIES list and validate before using XFF value.
  • Alternatively, use the real socket address always (since nginx already rewrites it) — configure nginx proxy_set_header X-Real-IP $remote_addr and use that instead.
HIGH
F-06 — No CSRF protection on POST /tx (transaction broadcast)
vitocoin/api.py : do_POST() line 920  |  app.html

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.

Recommended Fix

  • Replace Access-Control-Allow-Origin: * with an explicit allowlist of trusted origins.
  • For the frontend's own API calls, use a CSRF token header (X-VITO-Token) checked server-side.
  • Consider restricting transaction submission to authenticated (Bearer token) requests only.
MEDIUM
F-11 — Missing Content-Security-Policy header
vitocoin/api.py : _security_headers()

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.

Recommended Fix

  • Add CSP to Vercel: 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).
  • Add Strict-Transport-Security: max-age=63072000; includeSubDomains to the nginx config.
API Security — Passed Checks
  • All user-supplied identifiers validated with strict regex (_HEX64_RE, _ADDR_RE) — no injection vectors
  • Integer query params clamped with _int_param(lo, hi) — no integer overflow / path traversal
  • Static file path traversal blocked with os.path.realpath prefix check
  • SSRF protection for /peers/connect: private IP ranges blocked via _PRIVATE_NETS list
  • Request body capped at 1 MB (MAX_BODY) — no memory exhaustion from large bodies
  • Bearer token comparison uses hmac.compare_digest (constant-time, no timing oracle)
  • Nginx rate-limit: 60 GET/min, 10 POST/min, 5 SSE/min per IP (deployed on all 3 nodes)
4. Mining Security
MEDIUM
F-09 — Fake hashrate reporting bypasses 85% auto-ban threshold
vitocoin/api.py : _register_miner() line ~78

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

Attack Scenario

  • Attacker controls 90% of hashrate but reports 0 H/s → ban check never triggers.
  • Attacker reports 10 TH/s for legitimate miner IPs → legitimate miners get banned from the node.

Recommended Fix

  • Derive estimated hashrate from the rate of valid block submissions per IP, not from self-reported values.
  • Weight ban decisions on share-submission rate and difficulty of submitted work, not self-declared figures.
Mining Security — Passed Checks
  • All submitted blocks require valid SHA-256d PoW — hash must be < target (enforced in _validate_header)
  • Coinbase reward validated: miner cannot claim more than subsidy + fees
  • Difficulty bits verified against computed _next_bits() — cannot submit blocks with artificially easy difficulty
  • Difficulty retarget clamped to 4× per window — prevents time-warp attack from causing sudden difficulty drop
  • Median-time-past (MTP) enforced — prevents timestamp manipulation to inflate difficulty window
  • Block template (GBT) provided via GET /mining/template — browser miners can participate without full node
5. Node / Consensus Security
HIGH
F-04 — 51% attack risk: all 3 nodes mine to the same wallet
Architecture — node systemd config (all nodes mine to Sovereign Founder Wallet — re-keyed Apr 12 2026)

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.

Attack Scenario

  • Admin credentials leak → attacker controls 100% hashrate → unlimited double-spend, transaction censorship, or chain rewind.
  • Even without credential leak: the current network has no decentralisation. A single VPS provider outage takes down consensus.
  • Anyone can join the network and immediately achieve >50% if even one of the three nodes goes offline.

Recommended Fix

  • Open mining to the public — promote the /join-network guide aggressively to bootstrap external miners.
  • Use separate mining wallet addresses on each node (different HD derivation indices) to reduce single-key risk.
  • Implement a mining pool contract or merged-mining to attract external hashrate.
  • Consider a Proof-of-Stake or hybrid consensus mechanism while hashrate is low.
MEDIUM
F-08 — Sybil / Eclipse attack surface — minimal peer diversity
vitocoin/network.py : SEED_NODES, _on_getaddr()

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.

Attack Scenario

  • Attacker spins up 4 nodes, connects to a target node, and disconnects its honest peers → target node sees a manipulated chain.
  • With only 3 honest nodes and MAX_OUTBOUND=8, all outbound slots can be filled by adversarial nodes.

Recommended Fix

  • Point DNS seeds to resolvable IPs and add at least 5–10 geographically diverse seed nodes.
  • Implement an addr diversity check — refuse to connect to more than 1 peer per /16 subnet.
  • Add address anchoring (Bitcoin Core's anchor connections) for outbound slot reservation to known-good peers.
LOW
F-12 — Genesis block accepted with nonce=0 (no PoW validation)
vitocoin/blockchain.py : _create_genesis() ~line 407

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.

LOW
F-13 — Orphan pool eviction by count, not memory weight
vitocoin/blockchain.py : MAX_ORPHANS = 5_000

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.

Recommended Fix

  • Replace MAX_ORPHANS count check with a MAX_ORPHAN_BYTES = 64 * 1024 * 1024 (64 MB) weight cap.
  • Evict oldest/largest orphan when weight limit is reached.
Node / Consensus Security — Passed Checks
  • Network magic bytes validated on every message — cross-network packet injection blocked
  • Version handshake enforced — peers must complete verack before receiving chain data
  • Per-peer ban scoring with automatic disconnect on threshold (BAN_THRESHOLD)
  • Chain reorg depth limited to 100 blocks (MAX_REORG_DEPTH) — prevents deep chain rewind DoS
  • Block timestamp validated: must be > MTP(11) and < now+2h — prevents time-warp attacks
  • SSRF protection on /peers/connect: private and reserved IP ranges blocked
  • UFW firewall on all 3 nodes: only ports 22, 80, 6334 reachable from internet
  • Fail2ban active: 20 rate-limit hits/60s → 1h ban on ports 80 + 6333
Remediation Priority
PriorityFindingEffortAction
CRITICALF-01 Plaintext keys in localStorageMediumEncrypt wallet storage with AES-256-GCM + PBKDF2 before writing to localStorage
CRITICALF-02 Hardcoded reset secretLowRemove endpoint from production; move secret to env var if kept
HIGHF-05 Hardcoded merchant mnemonicLowMove to env var; rotate mnemonic immediately
HIGHF-03 XFF IP spoofingLowOnly trust XFF from known proxy IP (127.0.0.1)
HIGHF-06 No CSRF protectionLowRestrict CORS origin; add CSRF token for sensitive endpoints
HIGHF-04 51% attack riskHighPromote public mining; separate wallet keys per node
MEDIUMF-07 Within-block double-spendLowAdd spent_in_block set to _validate_block()
MEDIUMF-08 Eclipse / Sybil riskMediumAdd DNS seeds; subnet diversity check on peer connections
MEDIUMF-09 Fake hashrate reportingMediumDerive hashrate from valid block submissions, not self-report
MEDIUMF-10 PBKDF2 low iterationsLowUse ≥310k iterations for wallet-encryption KDF (separate from BIP-39)
MEDIUMF-11 Missing CSPLowAdd CSP header to Vercel config and nginx