Security Guidelines¶
Security best practices for developing and deploying the Odoo 15 system.
Overview¶
Security is everyone's responsibility. This guide covers secure coding practices, common vulnerabilities, and security configurations.
OWASP Top 10 for Odoo¶
1. Injection (SQL, Command)¶
Risk: Attackers execute malicious code through user input.
Vulnerable Code:
# BAD - SQL Injection
def search_partner(self, name):
self.env.cr.execute(f"SELECT * FROM res_partner WHERE name = '{name}'")
# BAD - Command Injection
import os
def process_file(self, filename):
os.system(f"cat {filename}")
Secure Code:
# GOOD - Parameterized query
def search_partner(self, name):
self.env.cr.execute("SELECT * FROM res_partner WHERE name = %s", [name])
# GOOD - Use ORM
def search_partner(self, name):
return self.env['res.partner'].search([('name', '=', name)])
# GOOD - Safe file handling
import subprocess
def process_file(self, filename):
# Validate filename first
if not filename.isalnum():
raise ValueError("Invalid filename")
subprocess.run(['cat', filename], check=True)
2. Broken Authentication¶
Risk: Weak passwords, session hijacking.
Best Practices:
# Enforce strong passwords in res.users
class ResUsers(models.Model):
_inherit = 'res.users'
@api.constrains('password')
def _check_password_strength(self):
for user in self:
if len(user.password) < 12:
raise ValidationError("Password must be at least 12 characters")
Configuration:
# odoo.conf
list_db = False # Hide database selector
admin_passwd = STRONG_PASS # Strong master password
3. Sensitive Data Exposure¶
Risk: Exposing passwords, API keys, personal data.
Bad Practice:
# BAD - Logging sensitive data
_logger.info(f"User logged in with password: {password}")
# BAD - Hardcoded credentials
API_KEY = "sk_live_12345"
Good Practice:
# GOOD - Never log sensitive data
_logger.info(f"User {user.login} logged in")
# GOOD - Use environment variables
import os
API_KEY = os.environ.get('API_KEY')
# GOOD - Use Odoo parameters
api_key = self.env['ir.config_parameter'].sudo().get_param('api.key')
4. XML External Entities (XXE)¶
Risk: Processing malicious XML.
Secure Configuration:
# GOOD - Safe XML parsing
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False,
no_network=True
)
tree = etree.parse(xml_file, parser)
5. Broken Access Control¶
Risk: Users accessing unauthorized resources.
Odoo Security Model:
# security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_my_model_user,my.model.user,model_my_model,base.group_user,1,0,0,0
access_my_model_manager,my.model.manager,model_my_model,my_module.group_manager,1,1,1,1
Record Rules:
<!-- security/rules.xml -->
<record id="rule_my_model_user" model="ir.rule">
<field name="name">Users see own records</field>
<field name="model_id" ref="model_my_model"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
6. Security Misconfiguration¶
Checklist:
# Production odoo.conf
list_db = False # Disable database listing
admin_passwd = SECURE # Strong admin password
proxy_mode = True # Behind reverse proxy
dbfilter = ^mydb$ # Restrict database access
7. Cross-Site Scripting (XSS)¶
Risk: Executing malicious scripts in user's browser.
Vulnerable Code:
Secure Code:
<!-- GOOD - Escaped output (default) -->
<div t-esc="user_input"/>
<!-- GOOD - If HTML needed, sanitize first -->
<div t-raw="sanitized_html"/>
# Sanitize HTML in Python
from odoo.tools import html_sanitize
safe_html = html_sanitize(user_input)
8. Insecure Deserialization¶
Risk: Executing malicious serialized objects.
Bad Practice:
Good Practice:
9. Using Components with Known Vulnerabilities¶
Best Practices: - Keep Odoo updated - Update Python dependencies regularly - Monitor security advisories
# Check for vulnerable packages
pip install safety
safety check
# Update dependencies
pip install --upgrade -r requirements.txt
10. Insufficient Logging & Monitoring¶
Good Logging:
import logging
_logger = logging.getLogger(__name__)
def sensitive_operation(self):
_logger.info(
f"User {self.env.user.login} performed sensitive operation "
f"on record {self.id} at {fields.Datetime.now()}"
)
Odoo-Specific Security¶
Access Rights¶
# security/ir.model.access.csv
# Format: id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
# Public (no group) - DANGEROUS, avoid
access_public,my.model.public,model_my_model,,1,0,0,0
# Specific group - RECOMMENDED
access_users,my.model.users,model_my_model,base.group_user,1,1,1,0
access_managers,my.model.managers,model_my_model,base.group_system,1,1,1,1
Record Rules (Row-Level Security)¶
<!-- Users can only see their own records -->
<record id="rule_own_records" model="ir.rule">
<field name="name">Own Records Only</field>
<field name="model_id" ref="model_my_model"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<!-- Managers can see all records -->
<record id="rule_manager_all" model="ir.rule">
<field name="name">Managers See All</field>
<field name="model_id" ref="model_my_model"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
</record>
Field-Level Security¶
class MyModel(models.Model):
_name = 'my.model'
# Sensitive field - only managers can see
salary = fields.Float(groups='base.group_system')
# Sensitive field - readonly for non-managers
credit_limit = fields.Float(
groups='base.group_user,base.group_system'
)
Sudo Usage¶
# BAD - Unnecessary sudo
def get_partner(self):
return self.sudo().env['res.partner'].search([])
# GOOD - Only when needed, with specific purpose
def get_system_config(self):
# Need sudo to read system parameters
return self.env['ir.config_parameter'].sudo().get_param('key')
# GOOD - Document why sudo is needed
def update_related_record(self):
# Sudo needed because user may not have write access to related model
self.sudo().related_id.write({'updated': True})
API Security¶
REST API Authentication¶
# Validate API key
def _validate_api_key(self, api_key):
if not api_key:
raise AccessDenied("API key required")
# Use constant-time comparison
import hmac
valid_key = self.env['ir.config_parameter'].sudo().get_param('api.key')
if not hmac.compare_digest(api_key, valid_key):
raise AccessDenied("Invalid API key")
Rate Limiting¶
from functools import wraps
import time
# Simple rate limiting
_rate_limit = {}
def rate_limit(max_calls=100, period=60):
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
key = f"{self.env.user.id}:{func.__name__}"
now = time.time()
if key in _rate_limit:
calls, start = _rate_limit[key]
if now - start < period:
if calls >= max_calls:
raise AccessDenied("Rate limit exceeded")
_rate_limit[key] = (calls + 1, start)
else:
_rate_limit[key] = (1, now)
else:
_rate_limit[key] = (1, now)
return func(self, *args, **kwargs)
return wrapper
return decorator
Input Validation¶
import re
from odoo.exceptions import ValidationError
class MyModel(models.Model):
_name = 'my.model'
email = fields.Char()
phone = fields.Char()
@api.constrains('email')
def _check_email(self):
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
for record in self:
if record.email and not re.match(email_pattern, record.email):
raise ValidationError("Invalid email format")
@api.constrains('phone')
def _check_phone(self):
# Only allow digits, spaces, and common symbols
phone_pattern = r'^[\d\s\-\+\(\)]+$'
for record in self:
if record.phone and not re.match(phone_pattern, record.phone):
raise ValidationError("Invalid phone format")
Infrastructure Security¶
Docker Security¶
# docker-compose.yml security settings
services:
odoo:
# Don't run as root
user: "1000:1000"
# Read-only filesystem where possible
read_only: true
tmpfs:
- /tmp
# Limit capabilities
cap_drop:
- ALL
# Security options
security_opt:
- no-new-privileges:true
Nginx Security Headers¶
# nginx/conf.d/security.conf
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Enable XSS filter
add_header X-XSS-Protection "1; mode=block" always;
# Control referrer
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
# HSTS (only with valid SSL)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
SSL/TLS Configuration¶
# Strong SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
Firewall Rules¶
# Allow only necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
Secrets Management¶
Environment Variables¶
# .env file (never commit!)
POSTGRES_PASSWORD=very_secure_password_here
ADMIN_PASSWORD=another_secure_password
API_KEY=sk_live_xxxxx
AWS_SECRET_KEY=xxxxx
Odoo System Parameters¶
# Store secrets in ir.config_parameter
self.env['ir.config_parameter'].sudo().set_param('api.key', 'secret')
# Retrieve secrets
api_key = self.env['ir.config_parameter'].sudo().get_param('api.key')
Never Commit Secrets¶
Security Checklist¶
Development¶
- No hardcoded credentials
- Input validation on all user data
- Output encoding/escaping
- Parameterized SQL queries
- Proper access control defined
- Sensitive data not logged
- Dependencies up to date
Deployment¶
- SSL/TLS enabled
- Strong passwords set
- Database listing disabled
- Firewall configured
- Security headers set
- Admin password changed
- Debug mode disabled
Operations¶
- Regular backups encrypted
- Access logs monitored
- Security updates applied
- Penetration testing scheduled
- Incident response plan ready
Incident Response¶
If Breach Suspected¶
- Isolate - Disconnect affected systems
- Preserve - Don't destroy evidence
- Investigate - Check logs, identify scope
- Remediate - Fix vulnerability, reset credentials
- Notify - Inform affected parties if required
- Document - Record incident and response
- Improve - Update security measures