Spaces:
Running
🔒 SECURITY: Add owner authorization check for moderation/group management skills
Browse filesFix 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.
- modules/api.py +102 -26
- modules/config.py +43 -0
- modules/skills_registry.py +18 -0
|
@@ -595,20 +595,52 @@ class LLMManager:
|
|
| 595 |
logger.info("🔧 [INIT] Together OK")
|
| 596 |
|
| 597 |
def _setup_openrouter(self):
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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', '
|
| 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 |
-
|
| 1264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 4645 |
-
|
| 4646 |
-
|
| 4647 |
-
|
| 4648 |
-
|
| 4649 |
-
|
| 4650 |
-
|
| 4651 |
-
|
| 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__}")
|
|
@@ -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",
|
|
@@ -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"]
|