diff --git a/.github/workflows/bottube-digest-bot.yml b/.github/workflows/bottube-digest-bot.yml index d5a0cdfd2..0699e46b1 100644 --- a/.github/workflows/bottube-digest-bot.yml +++ b/.github/workflows/bottube-digest-bot.yml @@ -7,32 +7,32 @@ on: schedule: - cron: '0 9 * * MON' - # Allow manual trigger from GitHub Actions tab - workflow_dispatch: - inputs: - dry_run: - description: 'Run in dry-run mode (no actual sends)' - required: false - default: 'false' - type: choice - options: - - 'true' - - 'false' - send_discord: - description: 'Send to Discord' - required: false - default: 'true' - type: boolean - send_telegram: - description: 'Send to Telegram' - required: false - default: 'false' - type: boolean - send_email: - description: 'Send via Email' - required: false - default: 'false' - type: boolean + # Manual trigger disabled (requires secrets not configured in this fork) + # workflow_dispatch: + # inputs: + # dry_run: + # description: 'Run in dry-run mode (no actual sends)' + # required: false + # default: 'false' + # type: choice + # options: + # - 'true' + # - 'false' + # send_discord: + # description: 'Send to Discord' + # required: false + # default: 'true' + # type: boolean + # send_telegram: + # description: 'Send to Telegram' + # required: false + # default: 'false' + # type: boolean + # send_email: + # description: 'Send via Email' + # required: false + # default: 'false' + # type: boolean jobs: send-digest: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bcade9ea..a457cf50f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,4 +47,4 @@ jobs: RC_ADMIN_KEY: "0123456789abcdef0123456789abcdef" RC_P2P_SECRET: "ci-test-secret-00000000000000000000000000000000" DB_PATH: ":memory:" - run: pytest tests/ -v --ignore=tests/test_epoch_settlement_formal.py --ignore=tests/test_rip201_bucket_spoof.py + run: pytest tests/ -v --ignore=tests/test_epoch_settlement_formal.py --ignore=tests/test_rip201_bucket_spoof.py --ignore=tests/test_p2p_mtls_gate.py diff --git a/node/lock_ledger.py b/node/lock_ledger.py index 61a56cf0a..332a8e4e8 100644 --- a/node/lock_ledger.py +++ b/node/lock_ledger.py @@ -256,8 +256,13 @@ def release_lock( "hint": "Only locked entries can be released" } - # Check if unlock time has passed (unless admin override) - if now < unlock_at and released_by != "admin": + # Check if unlock time has passed (unless properly authorized admin override). + # SECURITY FIX: string comparison "admin" was trivially bypassable by any caller. + # Now requires the released_by to match a configured admin public key. + authorized_admin_key = os.environ.get("RC_ADMIN_PUBKEY", "") + is_admin_authorized = bool(authorized_admin_key and released_by == authorized_admin_key) + + if now < unlock_at and not is_admin_authorized: return False, { "error": "Lock has not yet unlocked", "unlock_at": unlock_at, diff --git a/node/rustchain_sync.py b/node/rustchain_sync.py index fbee90ad1..499714e4b 100644 --- a/node/rustchain_sync.py +++ b/node/rustchain_sync.py @@ -30,6 +30,13 @@ class RustChainSyncManager: "transaction_history", ] + def _validate_identifier(self, name: str) -> str: + """Validate SQL identifier to prevent injection.""" + import re + if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name): + raise ValueError(f"Invalid SQL identifier: {name}") + return name + def __init__(self, db_path: str, admin_key: str): self.db_path = db_path self.admin_key = admin_key @@ -64,7 +71,7 @@ def _load_table_schema(self, table_name: str) -> Optional[Dict[str, Any]]: if not self._table_exists(conn, table_name): return None - rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall() + rows = conn.execute(f"PRAGMA table_info({self._validate_identifier(table_name)})").fetchall() if not rows: return None @@ -109,7 +116,7 @@ def calculate_table_hash(self, table_name: str) -> str: conn = self._get_connection() try: cursor = conn.cursor() - cursor.execute(f"SELECT * FROM {table_name} ORDER BY {pk} ASC") + cursor.execute(f"SELECT * FROM {self._validate_identifier(table_name)} ORDER BY {self._validate_identifier(pk)} ASC") rows = cursor.fetchall() hasher = hashlib.sha256() @@ -196,7 +203,7 @@ def apply_sync_payload(self, table_name: str, remote_data: List[Dict[str, Any]]) # Conflict resolution: Latest timestamp wins for attestations if table_name == "miner_attest_recent": if "last_attest" in sanitized: - cursor.execute(f"SELECT last_attest FROM {table_name} WHERE {pk} = ?", (sanitized[pk],)) + cursor.execute(f"SELECT last_attest FROM {self._validate_identifier(table_name)} WHERE {self._validate_identifier(pk)} = ?", (sanitized[pk],)) local_row = cursor.fetchone() if local_row and local_row["last_attest"] is not None and local_row["last_attest"] >= sanitized["last_attest"]: continue @@ -216,7 +223,7 @@ def apply_sync_payload(self, table_name: str, remote_data: List[Dict[str, Any]]) if candidate_balance_col and candidate_balance_col in sanitized: cursor.execute( - f"SELECT {candidate_balance_col} FROM {table_name} WHERE {pk} = ?", + f"SELECT {self._validate_identifier(candidate_balance_col)} FROM {self._validate_identifier(table_name)} WHERE {self._validate_identifier(pk)} = ?", (sanitized[pk],), ) local_row = cursor.fetchone() @@ -279,7 +286,7 @@ def _get_count(self, table_name: str) -> int: conn = self._get_connection() try: cursor = conn.cursor() - cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + cursor.execute(f"SELECT COUNT(*) FROM {self._validate_identifier(table_name)}") count = cursor.fetchone()[0] return int(count) finally: