Skip to main content

Anti-Patterns from Frappe & Odoo

Purpose: This document catalogs critical mistakes from Frappe and Odoo that Framework M must not repeat.

Structure:

  • Tackled = Already designed into Framework M (kept for reference)
  • ⚠️ Remaining = Still needs explicit implementation

✅ TACKLED ISSUES

These issues have design decisions in place. Listed for reference only.

#IssueSolutionWhere
1name as Primary Keyid (UUID) as PK, name as unique indexPhase 01, 02
2Global State (frappe.session.user)Explicit user_context parameterPhase 02
3Metadata in DatabaseCode-first Pydantic modelsPhase 01, 02
4Synchronous/BlockingAsync native (Litestar + SQLAlchemy Async)Phase 01
5Monkey PatchingEntrypoint-based overridesPhase 01
6No Dependency Injectiondependency-injector libraryPhase 01
7Weak/No Type Hints100% type hints, mypy --strictPhase 01
8Database-Specific FeaturesPortable SQLAlchemy types onlyPhase 02
9Implicit TransactionsExplicit transaction context managersPhase 02
10No Event Sourcing / Audit TrailEvery state change emits eventPhase 02, 04
11Hard-coded HooksEvent bus with multiple subscribersPhase 04
12Permission Checks in Business LogicRepository-level enforcementPhase 03
13Session-Only AuthStateless JWT/header-based authPhase 03
14eval()/exec() in User InputJMESPath/SimpleEval onlyPhase 10
15Hard Delete OnlySoft delete with deleted_atPhase 02
16Opt-out API ExposureOpt-in api_resource = TruePhase 03
17Timezone-Naive DatetimesDateTime(timezone=True) in SchemaMapperPhase 02
18N+1 Query Problemload_children_for_parents() (select_in_loading)Phase 02
19Unbounded QueriesDEFAULT_LIMIT=20, MAX_LIMIT=1000 enforcedPhase 02
22Unrestricted File UploadsFile DocType with extension whitelist, MIME validationPhase 06
25No Circuit Breaker (Email)EmailQueue with async processing, not blockingPhase 06
26No API VersioningURL-based versioning /api/v1/ prefixPhase 03
29Health Check Endpoints Missing/studio/api/health endpoint implementedPhase 07
30Caching Layer MissingRedisCacheAdapter + GenericRepository integrationPhase 02, 04
32Over-Customization Upgrade TrapOverride mechanism + event-based extensionPhase 08

⚠️ REMAINING ISSUES TO TACKLE

These issues need explicit implementation. Each includes problem, impact, and required solution.

20. Missing Rate Limiting

❌ The Problem (Frappe)

# No built-in rate limiting
# Attackers brute-force login, spam API, exhaust resources

🔥 Why It's Bad

  • Brute Force Attacks: Unlimited login attempts
  • Resource Exhaustion: API spam causes denial of service
  • Cost Explosion: Cloud bills skyrocket from abuse
  • Real-world: Security advisories list this as vulnerability

✅ Required Solution

# Multi-level rate limiting
@rate_limit(max_requests=5, window_seconds=60, key="ip") # Login
async def login(credentials: LoginRequest) -> Token:
...

@rate_limit(max_requests=100, window_seconds=60, key="user_id") # API
async def create_document(data: CreateRequest) -> Document:
...

Phase: 03/10 (Middleware) Priority: HIGH


21. Secrets in Logs

❌ The Problem (Frappe)

frappe.log(f"Login attempt: {username}, {password}")  # Oops!

🔥 Why It's Bad

  • Credential Leak: Passwords visible in log files
  • Compliance Violation: PII in logs breaks GDPR
  • Breach Amplification: Attacker reads logs, gets more access

✅ Required Solution

import structlog

REDACTED_FIELDS = {"password", "token", "secret", "api_key", "credit_card"}

def redact_processor(logger, method_name, event_dict):
for key in list(event_dict.keys()):
if key.lower() in REDACTED_FIELDS:
event_dict[key] = "[REDACTED]"
return event_dict

logger = structlog.wrap_logger(
structlog.get_logger(),
processors=[redact_processor, ...]
)

Phase: 10 (Logging) Priority: HIGH


23. Missing Request ID Tracing

❌ The Problem (Frappe)

# "Error in Job" - which request triggered it? Unknown.
# Can't correlate logs across services

🔥 Why It's Bad

  • Debugging Nightmare: Can't trace request → error
  • Distributed Tracing: Microservices can't correlate
  • Support Tickets: User says "it failed" - can't find the log

✅ Required Solution

class RequestIdMiddleware:
async def __call__(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())

# Add to logging context
structlog.contextvars.bind_contextvars(request_id=request_id)

response = await call_next(request)
response.headers["X-Request-ID"] = request_id

return response

# Every log includes request_id automatically
logger.info("Processing invoice", invoice_id=inv.id)
# {"request_id": "abc-123", "message": "Processing invoice", ...}

Phase: 03 (Middleware) Priority: MEDIUM


24. No Idempotency Keys for Mutations

❌ The Problem (Frappe)

# Network timeout → client retries → two invoices created!

🔥 Why It's Bad

  • Duplicate Records: Retries create multiple copies
  • Double Charges: Payment processed twice
  • Data Corruption: Inconsistent state

✅ Required Solution

@post("/api/v1/{doctype}")
async def create_document(
data: CreateRequest,
idempotency_key: str = Header(alias="Idempotency-Key", default=None),
cache: CacheProtocol = Depends(),
) -> Document:
if idempotency_key:
cached = await cache.get(f"idempotency:{idempotency_key}")
if cached:
return cached # Return same response

result = await repo.save(data)

if idempotency_key:
await cache.set(f"idempotency:{idempotency_key}", result, ttl=86400)

return result

Phase: 03 (API Layer) Priority: MEDIUM


27. Unsafe Deserialization

❌ The Problem (Frappe/Odoo)

# Odoo uses pickle for sessions (RCE!)
import pickle
data = pickle.loads(user_controlled_bytes) # Arbitrary code execution

# YAML !!python/object tag executes code
yaml.load(user_input, Loader=yaml.FullLoader) # Unsafe!

🔥 Why It's Bad

  • Remote Code Execution: Pickle can execute arbitrary Python
  • Complete Compromise: Attacker owns the server
  • Real-world: CVE-2022-XXXX (Odoo)

✅ Required Solution

# ✅ Use JSON only for user data
import json
data = json.loads(user_input) # Safe

# ✅ If YAML needed, use safe_load
import yaml
data = yaml.safe_load(user_input) # No code execution

# ❌ NEVER in production code:
pickle.loads(...)
yaml.load(..., Loader=yaml.FullLoader)

Phase: ALL (Code Audit) Priority: HIGH


28. Missing CSRF Protection

❌ The Problem (Frappe)

# CSRF tokens inconsistently applied
# Attacker page can POST to /api/ on behalf of logged-in user

🔥 Why It's Bad

  • Account Takeover: Attacker changes user's password
  • Data Theft: Attacker exports user's data
  • Financial Fraud: Attacker initiates transfers
  • Real-world: Security advisory for Frappe

✅ Required Solution

# 1. SameSite cookies (primary defense)
response.set_cookie(
key="session",
value=token,
httponly=True,
secure=True,
samesite="lax", # Or "strict"
)

# 2. Verify Origin header for mutations
async def csrf_check(request: Request):
if request.method in ("POST", "PUT", "DELETE"):
origin = request.headers.get("Origin")
if origin and origin not in ALLOWED_ORIGINS:
raise CSRFError("Origin not allowed")

Phase: 03 (Middleware) Priority: HIGH


31. MariaDB-Specific Issues (From Production)

❌ The Problem (Frappe)

# Frappe is tightly coupled to MariaDB
# Many production issues stem from MariaDB-specific problems:
# - InnoDB buffer pool misconfiguration
# - Slow queries due to missing indexes
# - Connection pool exhaustion

🔥 Why It's Bad

  • Real-world Incidents: Frappe Cloud downtime traced to MariaDB bugs
  • Scaling Limit: Vertical scaling only for single-node MariaDB
  • Recovery Time: Database issues require manual intervention

✅ Required Solution

# Framework M: Database agnostic + proper pool config
# 1. SQLAlchemy handles dialect differences
# 2. Proper connection pooling
engine = create_async_engine(
DATABASE_URL,
pool_size=20,
max_overflow=10,
pool_timeout=30,
pool_recycle=3600,
pool_pre_ping=True, # Health check connections
)

# 3. Read replicas for scaling
# Configured via Phase 10 multi-bind support

Phase: 02, 10 (Database) Priority: MEDIUM


Summary: Remaining Work

PriorityIssues
HIGH#18 N+1, #19 Limits, #20 Rate Limit, #21 Log Redaction, #22 File Uploads, #27 Deserialization, #28 CSRF
MEDIUM#23 Request ID, #24 Idempotency, #25 Circuit Breaker, #26 API Versioning, #29 Health Checks, #30 Caching, #31 DB Config, #32 Override Strategy

Checklist for Every Feature

Before implementing any feature, verify:

  • Datetimes are timezone-aware (UTC)?
  • Queries are paginated with max limit?
  • No N+1 queries (use eager loading)?
  • Rate limiting applied (if user-facing)?
  • Secrets redacted from logs?
  • File uploads validated (extension + magic bytes)?
  • Request ID propagated through logs?
  • No pickle/unsafe YAML?
  • CSRF protection on mutations?
  • Circuit breaker on external calls?