Isaac Quarenta commited on
Commit
e07e66f
·
1 Parent(s): 8385d40

🔒 SECURITY: Add owner authorization check for moderation/group management skills

Browse files

Fix critical security vulnerability where any user could execute
moderation and group management actions (kick, ban, mute, leave group,
change group settings, etc.) by simply asking the bot.

Changes:
- modules/config.py: Add OWNER_ID, RESTRICTED_SKILLS, and is_owner()
function to verify that only Isaac (ID: 202391978787009) can execute
restricted skills
- modules/api.py: Add authorization check in _execute_agent_loop before
executing any restricted skill, blocking non-owner users
- modules/skills_registry.py: Add defense-in-depth authorization check
in SkillRegistry.execute() to protect against bypass attempts

Restricted skills include: moderation, group_management, group_control,
delete_message, set_bot_profile, configure_moderation,
manage_moderation_exceptions, manage_blacklist, manage_warnings,
configure_welcome_goodbye, broadcast_message, reset_conversation_memory

Also includes pre-existing OpenRouter rotation improvements.

Files changed (3) hide show
  1. modules/api.py +102 -26
  2. modules/config.py +43 -0
  3. modules/skills_registry.py +18 -0
modules/api.py CHANGED
@@ -595,20 +595,52 @@ class LLMManager:
595
  logger.info("🔧 [INIT] Together OK")
596
 
597
  def _setup_openrouter(self):
598
- api_key = getattr(self.config, 'OPENROUTER_API_KEY', '')
599
- if api_key and len(api_key) > 5:
600
- try:
601
- import openai
602
- import httpx
603
- self.openrouter_client = openai.OpenAI(
604
- base_url="https://openrouter.ai/api/v1",
605
- api_key=api_key,
606
- timeout=httpx.Timeout(30.0, connect=8.0),
607
- max_retries=0,
608
- )
609
- logger.info("OpenRouter OK")
610
- except Exception as e:
611
- logger.warning(f"OpenRouter falhou: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  self.openrouter_client = None
613
 
614
  def _setup_torouter(self):
@@ -1163,8 +1195,11 @@ class LLMManager:
1163
  try:
1164
  rotation = get_openrouter_rotation()
1165
  current_name = rotation.get_current_account_name()
 
1166
  if current_name:
1167
  openrouter_account_label = current_name
 
 
1168
  except Exception:
1169
  pass
1170
 
@@ -1190,7 +1225,7 @@ class LLMManager:
1190
  messages.append(msg)
1191
  messages.append({"role": "user", "content": user_prompt or ""})
1192
 
1193
- model_name = getattr(self.config, 'OPENROUTER_MODEL', 'tencent/hy3-preview:free')
1194
 
1195
  try:
1196
  resp = self.openrouter_client.chat.completions.create(
@@ -1260,8 +1295,39 @@ class LLMManager:
1260
  status_match = None
1261
 
1262
  if status_match == 429 or "429" in err_str or "Too Many Requests" in err_str or "free-models-per-day" in err_str:
1263
- self.__class__._openrouter_circuit_open_until = _time.time() + self.__class__._OPENROUTER_CIRCUIT_TIMEOUT
1264
- logger.warning(f"⚡ [OR-CIRCUIT] 429 detectado na conta {openrouter_account_label} → OpenRouter bloqueado por {int(self.__class__._OPENROUTER_CIRCUIT_TIMEOUT//60)} min")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1265
  return None
1266
  elif status_match == 401 or "401" in err_str or "Unauthorized" in err_str:
1267
  logger.error("OpenRouter: Erro de autenticação (401). Pulando.")
@@ -4641,15 +4707,25 @@ class AkiraAPI:
4641
  }
4642
  })
4643
 
4644
- # Executa a skill (com injeção de contexto)
4645
- observation = registry.execute(
4646
- tc.name,
4647
- args,
4648
- analise_visao=analise_visao,
4649
- analise_doc=analise_doc,
4650
- conversation_id=conversation_id,
4651
- user_id=numero
4652
- )
 
 
 
 
 
 
 
 
 
 
4653
 
4654
  # 🔍 DEBUG EXTREMO: Log completo da observation
4655
  self.logger.info(f"🔍 [SKILL RESULT] {tc.name} = {type(observation).__name__}")
 
595
  logger.info("🔧 [INIT] Together OK")
596
 
597
  def _setup_openrouter(self):
598
+ try:
599
+ rotation = get_openrouter_rotation()
600
+ current_key = rotation.get_current_key()
601
+ current_name = rotation.get_current_account_name()
602
+
603
+ if not current_key:
604
+ # Fallback: usar OPENROUTER_API_KEY (single key)
605
+ current_key = getattr(self.config, 'OPENROUTER_API_KEY', '')
606
+ current_name = "default"
607
+
608
+ if current_key and len(current_key) > 5:
609
+ try:
610
+ import openai
611
+ import httpx
612
+ self.openrouter_client = openai.OpenAI(
613
+ base_url="https://openrouter.ai/api/v1",
614
+ api_key=current_key,
615
+ timeout=httpx.Timeout(30.0, connect=8.0),
616
+ max_retries=0,
617
+ )
618
+ logger.info(f"✅ OpenRouter OK (conta: {current_name})")
619
+ except Exception as e:
620
+ logger.warning(f"OpenRouter falhou: {e}")
621
+ self.openrouter_client = None
622
+ else:
623
+ logger.warning("⚠️ OpenRouter: Nenhuma chave API válida configurada")
624
+ self.openrouter_client = None
625
+ except Exception as e:
626
+ logger.warning(f"OpenRouter rotation falhou: {e}")
627
+ # Fallback: tentar OPENROUTER_API_KEY diretamente
628
+ api_key = getattr(self.config, 'OPENROUTER_API_KEY', '')
629
+ if api_key and len(api_key) > 5:
630
+ try:
631
+ import openai
632
+ import httpx
633
+ self.openrouter_client = openai.OpenAI(
634
+ base_url="https://openrouter.ai/api/v1",
635
+ api_key=api_key,
636
+ timeout=httpx.Timeout(30.0, connect=8.0),
637
+ max_retries=0,
638
+ )
639
+ logger.info("OpenRouter OK (fallback single key)")
640
+ except Exception as e2:
641
+ logger.warning(f"OpenRouter falhou: {e2}")
642
+ self.openrouter_client = None
643
+ else:
644
  self.openrouter_client = None
645
 
646
  def _setup_torouter(self):
 
1195
  try:
1196
  rotation = get_openrouter_rotation()
1197
  current_name = rotation.get_current_account_name()
1198
+ current_key = rotation.get_current_key()
1199
  if current_name:
1200
  openrouter_account_label = current_name
1201
+ if current_key:
1202
+ self.openrouter_client.api_key = current_key
1203
  except Exception:
1204
  pass
1205
 
 
1225
  messages.append(msg)
1226
  messages.append({"role": "user", "content": user_prompt or ""})
1227
 
1228
+ model_name = getattr(self.config, 'OPENROUTER_MODEL', 'deepseek/deepseek-chat')
1229
 
1230
  try:
1231
  resp = self.openrouter_client.chat.completions.create(
 
1295
  status_match = None
1296
 
1297
  if status_match == 429 or "429" in err_str or "Too Many Requests" in err_str or "free-models-per-day" in err_str:
1298
+ # [OR-ROTATION] Tentar rotacionar para próxima conta em vez de bloquear tudo
1299
+ retry_succeeded = False
1300
+ try:
1301
+ rotation = get_openrouter_rotation()
1302
+ next_key = rotation.rotate_on_429()
1303
+ if next_key:
1304
+ self.openrouter_client.api_key = next_key
1305
+ new_account = rotation.get_current_account_name()
1306
+ logger.info(f"🔄 [OR-ROTATION] Rotacionado para conta: {new_account}. Tentando novamente...")
1307
+ try:
1308
+ resp = self.openrouter_client.chat.completions.create(
1309
+ model=model_name,
1310
+ messages=messages,
1311
+ temperature=0.7,
1312
+ max_tokens=max_tokens
1313
+ )
1314
+ if resp and hasattr(resp, 'choices') and resp.choices:
1315
+ choice = resp.choices[0]
1316
+ if hasattr(choice.message, 'content') and choice.message.content:
1317
+ text = choice.message.content
1318
+ if text and isinstance(text, str) and text.strip():
1319
+ logger.info(f"✅ [OR-ROTATION] Sucesso na conta: {new_account}")
1320
+ return text.strip()
1321
+ except Exception as retry_err:
1322
+ logger.warning(f"⚠️ [OR-ROTATION] Retry falhou na conta {new_account}: {retry_err}")
1323
+ else:
1324
+ logger.warning("⚠️ [OR-ROTATION] Todas as contas OpenRouter esgotadas. Nenhuma chave disponível.")
1325
+ except Exception as rot_err:
1326
+ logger.debug(f"⚠️ OpenRouter rotation falhou: {rot_err}")
1327
+
1328
+ if not retry_succeeded:
1329
+ self.__class__._openrouter_circuit_open_until = _time.time() + self.__class__._OPENROUTER_CIRCUIT_TIMEOUT
1330
+ logger.warning(f"⚡ [OR-CIRCUIT] 429 detectado na conta {openrouter_account_label} → OpenRouter bloqueado por {int(self.__class__._OPENROUTER_CIRCUIT_TIMEOUT//60)} min")
1331
  return None
1332
  elif status_match == 401 or "401" in err_str or "Unauthorized" in err_str:
1333
  logger.error("OpenRouter: Erro de autenticação (401). Pulando.")
 
4707
  }
4708
  })
4709
 
4710
+ # 🔒 SECURITY: Authorization check for restricted skills
4711
+ # Only the owner (Isaac, ID: 202391978787009) can execute
4712
+ # moderation/group management actions
4713
+ if tc.name in config.RESTRICTED_SKILLS and not config.is_owner(numero):
4714
+ self.logger.warning(
4715
+ f"🔒 [SECURITY] Usuário {numero} (não-proprietário) "
4716
+ f"tentou executar skill restrita: {tc.name}. Bloqueado."
4717
+ )
4718
+ observation = "Não autorizado. Apenas o dono pode executar este comando."
4719
+ else:
4720
+ # Executa a skill (com injeção de contexto)
4721
+ observation = registry.execute(
4722
+ tc.name,
4723
+ args,
4724
+ analise_visao=analise_visao,
4725
+ analise_doc=analise_doc,
4726
+ conversation_id=conversation_id,
4727
+ user_id=numero
4728
+ )
4729
 
4730
  # 🔍 DEBUG EXTREMO: Log completo da observation
4731
  self.logger.info(f"🔍 [SKILL RESULT] {tc.name} = {type(observation).__name__}")
modules/config.py CHANGED
@@ -1013,6 +1013,27 @@ PRIVILEGED_USERS: Tuple[str, ...] = (
1013
  "202391978787009", # Added for full recognition
1014
  )
1015
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  # ============================================================
1017
  # 🎯 PROMPT DO SISTEMA (INJEÇÃO GARANTIDA EM TODOS OS PROVEDORES)
1018
  # ============================================================
@@ -1309,6 +1330,25 @@ def is_privileged(usuario_id: str) -> bool:
1309
  logger.debug(f"Usuário não privilegiado: {usuario_id}")
1310
  return False
1311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1312
  def verificar_privilegios_detalhado(usuario_id: str) -> Dict[str, Any]:
1313
  """
1314
  Verificação detalhada de privilégios com nível e permissões.
@@ -2584,6 +2624,9 @@ __all__: List[str] = [
2584
  "PRIVILEGED_COMMAND_PREFIXES",
2585
  "is_privileged",
2586
  "is_privileged_command",
 
 
 
2587
 
2588
  # API Status
2589
  "API_AVAILABLE",
 
1013
  "202391978787009", # Added for full recognition
1014
  )
1015
 
1016
+ # ============================================================
1017
+ # 🔒 OWNER ID - Apenas este usuário pode executar ações críticas
1018
+ # ============================================================
1019
+ OWNER_ID: str = "202391978787009"
1020
+
1021
+ # Habilidades restritas: apenas o proprietário (OWNER_ID) pode executar
1022
+ RESTRICTED_SKILLS: Tuple[str, ...] = (
1023
+ "moderation", # kick, ban, mute, clear
1024
+ "group_management", # get_invite_link, get_admins, get_members, etc.
1025
+ "group_control", # open, close, lock_settings, unlock_settings
1026
+ "delete_message", # delete messages
1027
+ "set_bot_profile", # change bot name/about
1028
+ "configure_moderation", # configure protections (antilink, antispam, etc.)
1029
+ "manage_moderation_exceptions", # manage exemptions
1030
+ "manage_blacklist", # manage blacklist
1031
+ "manage_warnings", # apply/remove warnings
1032
+ "configure_welcome_goodbye", # configure welcome/goodbye
1033
+ "broadcast_message", # broadcast to all members
1034
+ "reset_conversation_memory", # reset conversation memory
1035
+ )
1036
+
1037
  # ============================================================
1038
  # 🎯 PROMPT DO SISTEMA (INJEÇÃO GARANTIDA EM TODOS OS PROVEDORES)
1039
  # ============================================================
 
1330
  logger.debug(f"Usuário não privilegiado: {usuario_id}")
1331
  return False
1332
 
1333
+ def is_owner(usuario_id: str) -> bool:
1334
+ """
1335
+ Verifica se o usuário é o proprietário (OWNER_ID).
1336
+
1337
+ Apenas o proprietário pode executar habilidades restritas
1338
+ (moderação, gerenciamento de grupo, etc.).
1339
+
1340
+ Args:
1341
+ usuario_id: ID do usuário (número de telefone ou nome)
1342
+
1343
+ Returns:
1344
+ True se o usuário for o proprietário
1345
+ """
1346
+ if not usuario_id:
1347
+ return False
1348
+
1349
+ numero_limpo = re.sub(r'[^\d]', '', str(usuario_id))
1350
+ return numero_limpo == OWNER_ID
1351
+
1352
  def verificar_privilegios_detalhado(usuario_id: str) -> Dict[str, Any]:
1353
  """
1354
  Verificação detalhada de privilégios com nível e permissões.
 
2624
  "PRIVILEGED_COMMAND_PREFIXES",
2625
  "is_privileged",
2626
  "is_privileged_command",
2627
+ "OWNER_ID",
2628
+ "RESTRICTED_SKILLS",
2629
+ "is_owner",
2630
 
2631
  # API Status
2632
  "API_AVAILABLE",
modules/skills_registry.py CHANGED
@@ -61,10 +61,28 @@ class SkillRegistry:
61
 
62
  ✅ NOVO: Handler especial para media (images_data, video_url, audio_url)
63
  Evita error "Object of type bytes is not JSON serializable"
 
 
 
64
  """
65
  if name not in self.skills:
66
  return f"Erro: Skill '{name}' não encontrada."
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  try:
69
  logger.info(f"🚀 Executando Skill: {name} com args: {args}")
70
  handler = self.skills[name]["handler"]
 
61
 
62
  ✅ NOVO: Handler especial para media (images_data, video_url, audio_url)
63
  Evita error "Object of type bytes is not JSON serializable"
64
+
65
+ 🔒 SECURITY: Authorization check for restricted skills (defense-in-depth)
66
+ Only the owner can execute moderation/group management skills.
67
  """
68
  if name not in self.skills:
69
  return f"Erro: Skill '{name}' não encontrada."
70
 
71
+ # 🔒 SECURITY: Defense-in-depth authorization check
72
+ # This is a backup check in case _execute_agent_loop is bypassed
73
+ user_id = kwargs.get("user_id")
74
+ if user_id:
75
+ try:
76
+ from .config import RESTRICTED_SKILLS, is_owner
77
+ if name in RESTRICTED_SKILLS and not is_owner(user_id):
78
+ logger.warning(
79
+ f"🔒 [SECURITY] Usuário {user_id} (não-proprietário) "
80
+ f"tentou executar skill restrita: {name}. Bloqueado no registry."
81
+ )
82
+ return "Não autorizado. Apenas o dono pode executar este comando."
83
+ except Exception:
84
+ pass
85
+
86
  try:
87
  logger.info(f"🚀 Executando Skill: {name} com args: {args}")
88
  handler = self.skills[name]["handler"]