Segurança — Python SWE Agent
OWASP Top 10 — Aplicação em Python
A01 — Broken Access Control
# ❌ RUIM: sem verificação de ownership
@router.get("/orders/{order_id}")
async def get_order(order_id: UUID, current_user: User = Depends(get_current_user)):
return await order_repo.find_by_id(order_id) # qualquer usuário acessa qualquer pedido!
# ✅ BOM: verifica que o recurso pertence ao usuário
@router.get("/orders/{order_id}")
async def get_order(
order_id: UUID,
current_user: User = Depends(get_current_user),
use_case: GetOrderUseCase = Depends(),
):
order = await use_case.execute(order_id=order_id, requester_id=current_user.id)
if order is None:
raise HTTPException(status_code=404)
return order
# No use case:
class GetOrderUseCase:
async def execute(self, order_id: UUID, requester_id: UUID) -> Order | None:
order = await self._repo.find_by_id(order_id)
if order is None:
return None
if order.customer_id != requester_id:
raise ForbiddenError("You don't have access to this order")
return order
A02 — Cryptographic Failures
# ❌ NUNCA: MD5/SHA1 para senhas, dados sensíveis em texto plano
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest() # PROIBIDO
# ✅ BOM: bcrypt/argon2 para senhas
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def hash_password(plain: str) -> str:
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
# ✅ Dados sensíveis em trânsito: sempre HTTPS, nunca logar PII
# ❌ NUNCA logar:
logger.info(f"User logged in: email={user.email}, password={password}") # PROIBIDO
# ✅ BOM:
logger.info("User logged in", extra={"user_id": str(user.id)})
A03 — Injection
# ❌ RUIM: SQL raw com f-string
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'") # SQL INJECTION!
# ✅ BOM: ORM ou parameterized query
# Django ORM
User.objects.filter(email=email)
# SQLAlchemy
session.query(User).filter(User.email == email)
# Raw SQL seguro com parâmetros
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
# ❌ RUIM: eval/exec de input de usuário
eval(user_input) # NUNCA FAÇA ISSO
# ❌ RUIM: subprocess sem sanitização
import subprocess
subprocess.run(f"ls {user_path}", shell=True) # Command Injection!
# ✅ BOM: lista de argumentos, sem shell=True
subprocess.run(["ls", user_path], shell=False)
A07 — Authentication Failures
# JWT seguro com FastAPI
from jose import JWTError, jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = os.environ["JWT_SECRET_KEY"] # nunca hardcoded
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {"sub": subject, "exp": expire, "iat": datetime.now(timezone.utc)}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await user_repo.find_by_id(UUID(user_id))
if user is None:
raise credentials_exception
return user
A05 — Security Misconfiguration
# FastAPI — headers de segurança obrigatórios
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
# CORS restritivo em produção
app.add_middleware(
CORSMiddleware,
allow_origins=os.environ.get("ALLOWED_ORIGINS", "").split(","), # não "*" em prod!
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
# Remover headers que revelam stack
@app.middleware("http")
async def remove_server_header(request, call_next):
response = await call_next(request)
response.headers.pop("server", None)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
Gestão de Secrets
# ❌ NUNCA: secrets no código
DATABASE_URL = "postgresql://user:password123@localhost/db" # PROIBIDO
API_KEY = "sk-1234567890abcdef" # PROIBIDO
# ✅ BOM: sempre via variável de ambiente + validação no boot
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
database_url: str # obrigatório — falha no boot se ausente
secret_key: str
redis_url: str = "redis://localhost:6379"
# validação extra
@validator("secret_key")
def secret_key_must_be_strong(cls, v):
if len(v) < 32:
raise ValueError("SECRET_KEY must be at least 32 characters")
return v
settings = Settings() # lança exceção se variável obrigatória faltar
# .gitignore — SEMPRE incluir
.env
.env.local
.env.*.local
*.pem
*.key
secrets/
Validação de Input
# FastAPI + Pydantic v2 — validação automática
from pydantic import BaseModel, field_validator, EmailStr, constr
from typing import Annotated
class CreateUserRequest(BaseModel):
email: EmailStr
password: Annotated[str, constr(min_length=8, max_length=72)]
name: Annotated[str, constr(min_length=1, max_length=100, strip_whitespace=True)]
@field_validator("password")
@classmethod
def password_complexity(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError("Password must contain at least one uppercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain at least one digit")
return v
Ferramentas de Segurança — CI Obrigatório
# .github/workflows/security.yml
- name: Bandit (SAST)
run: bandit -r src/ -ll -ii
- name: Safety (CVEs em dependências)
run: safety check --full-report
- name: Semgrep
run: semgrep --config=p/python --error
- name: Trivy (container scan)
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: HIGH,CRITICAL
exit-code: 1
# Instalar e rodar localmente
pip install bandit safety semgrep
bandit -r src/ -ll # encontra problemas comuns de segurança
safety check # verifica CVEs nas dependências
semgrep --config=p/python . # análise estática avançada
Checklist de Segurança — Code Review