CuanDeOro
Findings · Production .ie · v2
← Hub

Findings para reparar · cuandeoro.ie

14 hallazgos · soluciones técnicas concretas · tu opinión/orden se guarda automático en este navegador

Estas son las reparaciones reales antes de consolidar producción en cuandeoro.ie. Cada fila tiene la solución técnica concreta y un campo donde puedes escribir tu opinión u orden de implementación. qwen-coder es quien escribirá el código real cuando autorices. Los botones "✓ arreglar / ⏳ después / ✕ descartar" rellenan tu respuesta rápido.
💾 Auto-guardado local: lo que escribas se guarda en este navegador (localStorage). NO se sube al servidor. Si limpias caché o cambias de navegador, lo pierdes. Usa "Exportar respuestas" para tener una copia.

Secretos y credenciales · los más urgentes (CRITICAL)

IDSevProblemaExplicaciónSolución propuestaTu opinión / orden
F-01Critical archivos_cuandeoro/.env con credenciales texto plano ARCHIVOS_USER=marco y ARCHIVOS_PASS=cuandeoro2026 visibles a cualquiera con read en el server. Viola R90-KMS. 1. Rotar passwordNEWPASS=$(openssl rand -base64 24)2. Guardar cifrado en Vault Transitvault write transit/encrypt/profitnew-master \ plaintext=$(echo -n "$NEWPASS" | base64) \ -format=json | jq -r .data.ciphertext > ARCHIVOS_PASS.enc3. Sustituir .envARCHIVOS_USER=marco ARCHIVOS_PASS_ENC=vault:v1:kjdf...4. server.py lee y descifra al boot vía AppRole
F-02Critical Keypair Solana hardcoded en bridge.mjs Línea 7: path absoluto al keypair JSON. Si comprometen el código, drenan la wallet GDGQ...AUF. 1. Subir keypair a Vault KVvault kv put secret/cuandeoro/wallets/dallas_hot \ keypair=@/home/admin/.velocityquant_secrets/dallas_hot_keypair.json2. bridge.mjs usa node-vaultimport vault from 'node-vault'; const v = vault({ endpoint:'http://127.0.0.1:8200', token:process.env.VAULT_TOKEN }); const { data } = await v.read('secret/data/cuandeoro/wallets/dallas_hot'); const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(data.data.keypair)));3. Eliminar archivo plano del filesystem tras validar.
F-03Critical Semilla BIP-39 deployer en TOML accesible finish.mjs:12 y open_trustline.mjs:5 leen seed phrase. Control TOTAL wallet GBL4...HVVA. 1. Mover seed phrase a Vault KVvault kv put secret/cuandeoro/deployer \ seed_phrase="word1 word2 ... word24"2. finish.mjs / open_trustline.mjs leen asíconst { data } = await v.read('secret/data/cuandeoro/deployer'); const seed = data.data.seed_phrase; const keypair = StellarSdk.Keypair.fromSeed(seed);3. Borrar TOMLshred -uvz /home/admin/.velocityquant_secrets/cuandeoro_deployer_mainnet.toml
F-04Critical setup_vault_cuandeoro_master.sh imprime credenciales a stdout Líneas 73 y 78 hacen echo $SECRET_ID. Quedan en bash_history y logs systemd. Reemplazar bloque inseguro por escritura cifrada a archivo:# En vez de: echo "ROLE_ID=$ROLE_ID" echo "SECRET_ID=$SECRET_ID" # Usar: umask 077 cat > /etc/vault.d/approle.env <
F-05Critical VAULT_TOKEN como env var visible en ps aux Script pide export VAULT_TOKEN=hvs.xxxxx. Token visible a otros usuarios + en bash_history. Usar archivo con permisos 600:# Al inicio del script en vez de "export": TOKEN_FILE="${HOME}/.vault-token" if [ ! -f "$TOKEN_FILE" ]; then read -rs -p "VAULT_TOKEN: " VAULT_TOKEN echo umask 077 echo "$VAULT_TOKEN" > "$TOKEN_FILE" chmod 600 "$TOKEN_FILE" fi export VAULT_TOKEN=$(cat "$TOKEN_FILE")Después borrar de bash_history:history -d $(history | grep "VAULT_TOKEN" | awk '{print $1}') history -w

Código Bridge Stellar (HIGH)

IDSevProblemaExplicaciónSolución propuestaTu opinión / orden
F-06High bridge.mjs:45 envía tx sin validar contenido sdk.bridge.rawTxBuilder.send(sendParams) sin checks previos. Tx malformada → fondos perdidos/bloqueados. Validar sendParams con Zod o validación manual antes del send:import { z } from 'zod'; const SendSchema = z.object({ amount: z.string().regex(/^\d+(\.\d{1,7})?$/), // Stellar 7 decimales max destination: z.string().regex(/^G[A-Z2-7]{55}$/), // ed25519 pubkey asset: z.union([z.literal('XLM'), z.string().regex(/^[A-Z0-9]{1,12}:G[A-Z2-7]{55}$/)]), memo: z.string().max(28).optional() }); const validated = SendSchema.parse(sendParams); if (parseFloat(validated.amount) <= 0) throw new Error('amount must be > 0'); if (validated.destination === SOURCE_PUBKEY) throw new Error('self-send blocked'); const rawTx = await sdk.bridge.rawTxBuilder.send(validated);

Contratos Soroban (HIGH)

IDSevProblemaExplicaciónSolución propuestaTu opinión / orden
F-07High Sumas i128 sin checked_add Struct Amounts (lib.rs:120-130). Atacante manda amounts que sumen > balance → overflow → estado parcial. En lib.rs función prepare(), reemplazar sum por checked:// Antes: // let total: i128 = amounts.iter().map(|a| a.value).sum(); // Después: let mut total: i128 = 0; for a in amounts.iter() { total = total .checked_add(a.value) .ok_or(Error::AmountOverflow)?; } // Y verificar contra balance: let buyer_balance = token_client.balance(&buyer); if total > buyer_balance { return Err(Error::InsufficientBalance.into()); }
F-08High Admin key sin time-lock ni multi-sig initialize requiere auth pero upgrade es instantáneo. Si admin key se compromete, atacante reemplaza contrato. Time-lock 48h en upgrades + multi-sig 2-of-3:#[contracttype] pub struct UpgradeProposal { new_wasm_hash: BytesN<32>, proposed_at: u64, signatures: Vec<Address>, // requiere 2 de 3 admins } const TIMELOCK_SECS: u64 = 172_800; // 48h const REQUIRED_SIGS: u32 = 2; pub fn propose_upgrade(env: Env, new_wasm: BytesN<32>, admin: Address) { admin.require_auth(); env.storage().instance().set(&UPGRADE_KEY, &UpgradeProposal { new_wasm_hash: new_wasm, proposed_at: env.ledger().timestamp(), signatures: vec![&env, admin], }); } pub fn execute_upgrade(env: Env) { let p: UpgradeProposal = env.storage().instance().get(&UPGRADE_KEY).unwrap(); let elapsed = env.ledger().timestamp() - p.proposed_at; if elapsed < TIMELOCK_SECS { panic!("timelock not elapsed"); } if p.signatures.len() < REQUIRED_SIGS { panic!("insufficient sigs"); } env.deployer().update_current_contract_wasm(p.new_wasm_hash); }

KYC PostgreSQL + GDPR (HIGH)

IDSevProblemaExplicaciónSolución propuestaTu opinión / orden
F-09High Sin audit_log de lecturas en kyc_records Exfiltración masiva de PII cifrado sin trazabilidad. Si roban KEK Vault después → descifran todo. Vista controlada + función SECURITY DEFINER que loggea:-- 1. Revoke direct access REVOKE SELECT ON kyc_records FROM app_role; -- 2. Función SECURITY DEFINER que loggea cada acceso CREATE OR REPLACE FUNCTION kyc_read(p_user_id UUID) RETURNS TABLE (passport_enc BYTEA, tax_id_enc BYTEA, status TEXT) LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN INSERT INTO kyc_audit_log(actor, action, target_user_id, ts) VALUES (current_user, 'READ_KYC', p_user_id, NOW()); RETURN QUERY SELECT k.passport_number_enc, k.tax_id_enc, k.kyc_status FROM kyc_records k WHERE k.user_id = p_user_id; END $$; -- 3. Solo app puede llamar la función GRANT EXECUTE ON FUNCTION kyc_read(UUID) TO app_role;
F-10High gdpr_purge_kyc_record editable evade auditoría Función modificable. Atacante con permisos comenta el INSERT al audit log → borra KYC sin evidencia. Ownership inmutable + revoke ALTER:-- 1. Mover función a role auditor protegido CREATE ROLE kyc_auditor NOLOGIN; ALTER FUNCTION gdpr_purge_kyc_record(UUID) OWNER TO kyc_auditor; -- 2. Revoke ALTER de app_role REVOKE ALL ON FUNCTION gdpr_purge_kyc_record(UUID) FROM app_role; GRANT EXECUTE ON FUNCTION gdpr_purge_kyc_record(UUID) TO app_role; -- 3. SECURITY DEFINER + search_path fijo (anti hijack) CREATE OR REPLACE FUNCTION gdpr_purge_kyc_record(p_user_id UUID) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, public AS $$ BEGIN INSERT INTO kyc_audit_log(actor, action, target_user_id, ts) VALUES (current_user, 'GDPR_PURGE', p_user_id, NOW()); UPDATE kyc_records SET passport_number_enc = NULL, tax_id_enc = NULL, deleted_at = NOW(), purge_reason = 'GDPR_ART17' WHERE user_id = p_user_id; END $$;
F-11High Borrado físico sin soft delete Conflicto GDPR Art.17 (borrar) vs AML5 (conservar 5 años). Solución híbrida: borrar PII pero retener metadata mínima. ALTER table + política híbrida:-- 1. Añadir columnas tombstone ALTER TABLE kyc_records ADD COLUMN deleted_at TIMESTAMPTZ, ADD COLUMN purge_reason TEXT, ADD COLUMN tx_hash_anchor BYTEA; -- hash on-chain como prueba -- 2. Modificar gdpr_purge: borra PII, mantiene anchor UPDATE kyc_records SET passport_number_enc = NULL, tax_id_enc = NULL, full_name_enc = NULL, -- mantenemos: user_id, deleted_at, tx_hash_anchor, kyc_outcome deleted_at = NOW(), purge_reason = 'GDPR_ART17' WHERE user_id = $1; -- 3. Excluir borrados de queries normales por defecto CREATE VIEW kyc_active AS SELECT * FROM kyc_records WHERE deleted_at IS NULL; -- 4. Job nocturno: hard-delete después de 5 años AML DELETE FROM kyc_records WHERE deleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL '5 years';

Offramp (CRITICAL/HIGH)

IDSevProblemaExplicaciónSolución propuestaTu opinión / orden
F-12Critical Slippage 800 bps VE + 500 bps AR explotable Anchor malicioso cotiza barato → sube tasa en ejecución → fuerza reembolso → arbitraje encubierto drena wallet. Bajar tolerance + max-loss-per-tx + verificar oracle dual:# slippage_strategy.md update country_tolerance: VE: 300 # antes 800 - bajar a 3% AR: 200 # antes 500 - bajar a 2% default: 100 # código offramp - antes de aceptar quote: def validate_quote(quote, market_oracle, secondary_oracle): drift_primary = abs(quote.rate - market_oracle.rate) / market_oracle.rate * 10000 drift_secondary = abs(quote.rate - secondary_oracle.rate) / secondary_oracle.rate * 10000 if drift_primary > country_tolerance[quote.country]: raise SlippageExceeded(country=quote.country, bps=drift_primary) if drift_secondary > country_tolerance[quote.country] * 1.2: raise OracleDiscrepancy(primary=market_oracle.rate, secondary=secondary_oracle.rate) if quote.amount * drift_primary / 10000 > MAX_LOSS_PER_TX_USD: raise MaxLossExceeded() return True
F-13High Oracle secundario "implementación pendiente" Three-tier price oracle dice "pendiente" para validación cross-source. Mientras no exista, anchor único reporta tasas falsas sin contradicción. Implementar oracle secundario antes del primer offramp real:# Opciones gratuitas/baratas para FX rates: # 1. exchangerate.host (free, sin API key) # 2. Frankfurter API (free, ECB rates) # 3. Wise/Revolut públicos (rate limit) import httpx from typing import Tuple async def fetch_secondary_oracle(base: str, quote: str) -> float: async with httpx.AsyncClient(timeout=5.0) as c: # ECB via Frankfurter (no API key) r = await c.get(f"https://api.frankfurter.app/latest?from={base}&to={quote}") return r.json()["rates"][quote] async def get_dual_oracle(base: str, quote: str) -> Tuple[float, float]: primary = await anchor_quote(base, quote) secondary = await fetch_secondary_oracle(base, quote) return primary, secondary # Bloqueo si discrepancia > 5% primary, secondary = await get_dual_oracle("USD", "VES") if abs(primary - secondary) / secondary > 0.05: alert_ops("Oracle divergence", primary=primary, secondary=secondary) raise OracleDiscrepancy()
F-14High quote_id sin nonce / sin uso-único Atacante intercepta quote_id válido y lo reenvía después de cambiar tasa → reembolsos repetidos. Nonce + TTL + tabla quotes_consumed UNIQUE:-- DB CREATE TABLE quotes ( quote_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), nonce BYTEA NOT NULL DEFAULT gen_random_bytes(32), amount NUMERIC NOT NULL, rate NUMERIC NOT NULL, expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ, CONSTRAINT one_use UNIQUE (quote_id) -- ya es PK pero explícito ); CREATE INDEX idx_quotes_expiry ON quotes(expires_at) WHERE consumed_at IS NULL; -- Función atómica: consume si válido, lanza si ya usado o expirado CREATE FUNCTION consume_quote(p_quote_id UUID, p_nonce BYTEA) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ DECLARE rows_updated INT; BEGIN UPDATE quotes SET consumed_at = NOW() WHERE quote_id = p_quote_id AND nonce = p_nonce AND consumed_at IS NULL AND expires_at > NOW(); GET DIAGNOSTICS rows_updated = ROW_COUNT; RETURN rows_updated = 1; END $$; -- TTL corto típico -- TTL = 90 segundos para tx normal, 30s para mercados VE/AR volátiles
Orden recomendado si no quieres pensarlo
  1. F-01 rotar password .env (~1 min)
  2. F-04, F-05 arreglar script Vault (~5 min)
  3. F-02 + F-03 juntos: keypair + seed a Vault (~30 min)
  4. F-06 validación bridge (~10 min)
  5. F-09 + F-11 juntos: audit log + soft delete (~20 min)

Eso cierra los críticos de credenciales + los más explotables. ~1.5h con qwen-coder. F-07/F-08 (Soroban) y F-12/F-13/F-14 (offramp) son estructurales, mejor sesión dedicada.

Guardado en navegador