Quick Answer
Odoo automatically protects against CSRF attacks using unique tokens embedded in every form - zero code needed. The attack: User logged into Odoo, clicks link in phishing email, link contains hidden form submitting to your Odoo (delete order, change address, modify data), form uses user's session cookie, server processes request thinking it's legitimate = unauthorized action executed, data destroyed. Without CSRF protection: Attack succeeds, order deleted, user "I didn't delete that!", result: account compromised, data destroyed. With Odoo CSRF (automatic): (1) Server generates unique token per session ("abc123def456..."), (2) Embeds token in form (hidden input), (3) User submits legitimate request with token, (4) Server checks: token matches? ✓ Accept, (5) Attacker submits malicious request without token, (6) Server checks: token matches? ✗ Reject = attack prevented, data safe. How it works: Attacker can't know the token (random, unpredictable, session-specific), malicious form lacks token, Odoo rejects tokenless requests. When to disable: csrf=False ONLY for external integrations (payment webhooks can't include token), public APIs, mobile apps. Critical: When disabling, MUST implement alternatives: signature verification (webhooks with hmac.compare_digest), JWT tokens (mobile apps), API keys, rate limiting, IP whitelisting. NEVER csrf=False without alternative protection. Real patterns: Forms use CSRF (default, safe), webhooks use csrf=False + signature check (Stripe webhook verifies signature), mobile APIs use csrf=False + JWT validation. Impact: Understanding CSRF = zero unauthorized actions, users trust system. Disabling without alternatives = accounts hijacked, $200k-$1M damages, completely preventable.
The CSRF Attack Explained
Your D2C user is logged into their Odoo account. While they're browsing, they click a link from a phishing email. Unbeknownst to them, the link contains a malicious form that submits a request to YOUR Odoo instance:
<form action="https://your-odoo.com/api/orders/delete" method="POST">
<input type="hidden" name="order_id" value="12345">
<script>document.forms[0].submit();</script>
</form>
Without CSRF Protection
1. User clicks malicious link
2. Form submits to your Odoo using their session
3. Odoo processes request (thinks it's legitimate)
4. Order #12345 deleted
5. User: "I didn't delete that order!"
Result: Unauthorized action executed, data destroyed.
With CSRF Protection (Odoo Default)
1. User clicks malicious link
2. Form submits, but LACKS CSRF token
3. Odoo checks for token: MISSING
4. Odoo REJECTS request
5. No data modified
Result: Attack prevented, data safe.
The difference: Account compromised & data destroyed vs. bulletproof protection. And Odoo does it automatically (zero code needed). We've implemented 150+ Odoo systems. The ones where developers understand CSRF? Zero unauthorized account actions, users trust the system. The ones that disable CSRF without proper protection? Accounts hijacked, unauthorized actions, customer data modified, $200K-$1M in damages. That's completely preventable.
How CSRF Attacks Work (Step by Step)
| Step | Action | What Happens |
|---|---|---|
| 1 | You log into your bank | You're authenticated, browser has session cookie |
| 2 | Click link in phishing email | Attacker's site loads |
| 3 | Attacker's site contains hidden form | Form targets bank.com/transfer with attacker's account |
| 4 | Form auto-submits using YOUR session | Browser sends your session cookie automatically |
| 5 | Bank processes transfer | Sees valid session, thinks it's you |
| 6 | $10,000 transferred to attacker | Unauthorized transaction from your account |
Why CSRF Works
✓ You're logged in (browser has session cookie)
✓ Browser automatically sends cookie with request
✓ Server sees valid session, processes request
✓ You didn't authorize it (but server doesn't know)
Real D2C Scenario
1. Customer logged into Odoo (viewing their orders)
2. Clicks link in phishing email: "Your package is here!"
3. Link contains hidden form that deletes orders or changes delivery address
4. All executed using customer's session
5. Customer loses orders, becomes frustrated
Result: Data corruption, customer loss of trust.
How Odoo CSRF Tokens Work
CSRF Token: Unique token that proves request came from legitimate user, not attacker.
The Token Flow
Random, unpredictable: "abc123def456ghi789jkl012"
Stored in user's session memory
<form>
<input type="hidden" name="csrf_token"
value="abc123def456ghi789jkl012">
...
</form>
POST /api/orders/delete
csrf_token=abc123def456ghi789jkl012
order_id=12345
Form token: abc123def456ghi789jkl012
Session token: abc123def456ghi789jkl012
MATCH ✓ → Request accepted
POST /api/orders/delete
csrf_token=MISSING
order_id=12345
(Attacker doesn't know the token!)
Form token: MISSING
Session token: abc123def456ghi789jkl012
NO MATCH ✗ → Request REJECTED
Result: Only legitimate requests (with correct token) succeed. Attacks fail.
Using CSRF Tokens in Odoo
In Forms (Automatic - Zero Code Needed)
<!-- Odoo automatically includes CSRF token -->
<form method="post" action="/api/orders/create">
<!-- Odoo automatically adds this: -->
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<input type="text" name="order_name" placeholder="Order Name">
<button type="submit">Create Order</button>
</form>
Odoo does it automatically. You don't need to do anything. The token is:
• Generated per session
• Embedded in every form
• Validated on submission
• Rejected if missing or wrong
In JavaScript (AJAX Requests)
// Get CSRF token from page
var csrfToken = document.querySelector('input[name="csrf_token"]').value;
// Or from core
var csrfToken = odoo.web.csrf_token;
// Include in AJAX request
fetch('/api/orders/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // Send as header
},
body: JSON.stringify({
name: 'New Order',
amount: 1000
})
});
When to Disable CSRF (Advanced)
⚠️ Warning:
Only disable CSRF for external integrations. ALWAYS implement alternative security.
Valid Reasons to Disable
• External integrations (payment gateway webhooks)
• Public APIs (no authentication)
• Mobile apps (use API tokens instead)
How to Disable (With Alternative Security)
# In your route definition
@http.route('/api/webhook/stripe', type='json', auth='public', csrf=False)
def stripe_webhook(self, **kwargs):
"""Stripe sends webhooks (no CSRF token possible)."""
# But implement alternative security!
# Verify webhook signature
signature = request.headers.get('Stripe-Signature')
# This is critical - verify signature
try:
event = stripe.Webhook.construct_event(
request.data, # Raw body
signature,
STRIPE_WEBHOOK_SECRET # Secret key
)
except ValueError:
# Invalid signature
return {'error': 'Invalid signature'}, 403
# Process webhook safely
return {'success': True}
CRITICAL: When csrf=False, MUST implement alternative security:
✓ API key validation
✓ Signature verification (webhooks)
✓ Rate limiting
✓ IP whitelisting
✓ OAuth/JWT tokens
NEVER use csrf=False without alternative protection!
Real D2C Implementation Patterns
Pattern 1: Secure Form (CSRF Enabled)
<!-- views/order_form.xml -->
<record id="view_order_form" model="ir.ui.view">
<field name="arch" type="xml">
<form method="post" action="/orders/create">
<!-- CSRF token auto-included by Odoo -->
<input type="text" name="customer_name" required>
<input type="email" name="customer_email" required>
<button type="submit">Create Order</button>
</form>
</field>
</record>
Pattern 2: Webhook with Signature Verification
# controllers/payment_webhook.py
import hmac
import hashlib
class PaymentWebhook(http.Controller):
@http.route('/webhook/payment', type='json',
auth='public', csrf=False)
def handle_payment_webhook(self):
"""Handle payment gateway webhook (no CSRF token)."""
# Step 1: Verify webhook signature (alternative to CSRF)
signature = request.headers.get('X-Webhook-Signature')
body = request.get_data()
secret = os.environ.get('WEBHOOK_SECRET')
# Calculate expected signature
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
# Compare signatures (timing-safe)
if not hmac.compare_digest(signature, expected):
_logger.warning(f"Invalid webhook signature from {request.remote_addr}")
return {'error': 'Invalid signature'}, 403
# Step 2: Process webhook
data = request.get_json()
try:
order = self.env['sale.order'].browse(data['order_id'])
order.write({'payment_state': 'paid'})
# Log webhook
self.env['payment.log'].create({
'order_id': order.id,
'action': 'webhook_received',
'timestamp': fields.Datetime.now(),
})
return {'success': True}
except Exception as e:
_logger.error(f"Webhook processing failed: {e}")
return {'error': str(e)}, 500
Pattern 3: Mobile API with JWT Tokens
# controllers/mobile_api.py
import jwt
class MobileAPI(http.Controller):
@http.route('/api/mobile/orders', type='json',
auth='none', csrf=False)
def get_orders(self):
"""Mobile app API (uses JWT, not CSRF)."""
# Step 1: Verify JWT token (alternative to CSRF)
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return {'error': 'Missing authorization'}, 401
token = auth_header.split(' ')[1]
try:
payload = jwt.decode(
token,
os.environ.get('JWT_SECRET'),
algorithms=['HS256']
)
user_id = payload['user_id']
except jwt.InvalidTokenError:
return {'error': 'Invalid token'}, 401
# Step 2: Fetch user's orders
user = self.env['res.users'].browse(user_id)
orders = self.env['sale.order'].search([
('partner_id', '=', user.partner_id.id)
])
return {
'orders': [
{
'id': o.id,
'name': o.name,
'amount': o.amount_total,
'state': o.state,
}
for o in orders
]
}
Your Action Items
For Regular Forms (Safe by Default)
❏ Do NOTHING - Odoo handles CSRF automatically
❏ Just use normal forms
❏ Token embedded automatically
For External APIs (Need Alternatives)
❏ Use csrf=False ONLY for webhooks, public APIs, mobile apps
❏ Implement signature verification (webhooks with hmac.compare_digest)
❏ Implement JWT/API keys (mobile apps)
❏ Add rate limiting and IP whitelisting
Ongoing Security
❏ Never disable CSRF without alternative protection
❏ Use strongest auth available (JWT > API key > IP)
❏ Log all unauthorized attempts
❏ Monitor for CSRF attacks
Security Comparison
| Scenario | CSRF Protection | Alternative Security | Status |
|---|---|---|---|
| Regular Forms | Enabled (automatic) | Not needed | ✓ Safe |
| AJAX Requests | Enabled (X-CSRF-Token header) | Not needed | ✓ Safe |
| Payment Webhooks | Disabled (csrf=False) | Signature verification | ✓ Safe |
| Mobile APIs | Disabled (csrf=False) | JWT tokens | ✓ Safe |
| Public API (No Auth) | Disabled (csrf=False) | None | ✗ UNSAFE |
Frequently Asked Questions
What is CSRF and how does it attack Odoo applications?
CSRF (Cross-Site Request Forgery) tricks authenticated users into executing unauthorized actions by submitting malicious requests using their session. Attack flow: (1) User logs into Odoo (authenticated with session cookie), (2) User clicks phishing email link to attacker's site, (3) Attacker's site contains hidden form targeting your Odoo (<form action="https://your-odoo.com/api/orders/delete"> with order_id=12345), (4) Form auto-submits using JavaScript, (5) Browser automatically sends user's session cookie with request, (6) Odoo sees valid session and processes request (thinks it's legitimate), (7) Order deleted without user authorization. Why it works: Browser automatically includes cookies with all requests to the domain, server can't distinguish between legitimate user-initiated request and attacker-initiated request if only checking session cookie. Real D2C impact: Customer viewing orders clicks "package tracking" phishing link, attacker's form deletes orders or changes delivery address using customer's session = data corruption, trust loss, customer frustration.
How does Odoo's automatic CSRF protection work?
Odoo auto-generates unique CSRF tokens per session, embeds them in forms, and validates on submission - zero code needed. Token flow: (1) Server generates random unpredictable token ("abc123def456...") when session starts, (2) Stores token in session memory, (3) Automatically embeds token in all forms (<input type="hidden" name="csrf_token" value="abc123...">), (4) Legitimate user submits form with embedded token, (5) Server validates: form token matches session token? Yes ✓ = accept request, (6) Attacker submits malicious form without token (can't know the random session-specific token), (7) Server validates: token missing or wrong? No ✗ = reject request. Why attackers fail: Token is random (unpredictable), session-specific (different per user), not accessible cross-origin (attacker can't read it from their site). Odoo implementation: request.csrf_token() in QWeb templates, odoo.web.csrf_token in JavaScript, X-CSRF-Token header for AJAX. Result: 100% automatic protection, developers do nothing, all state-changing requests (POST/PUT/DELETE) validated.
When should I disable CSRF protection with csrf=False?
Only disable CSRF for external integrations (webhooks, mobile APIs) that cannot include tokens, and MUST implement alternative security. Valid reasons: (1) Payment gateway webhooks (Stripe, PayPal send POST requests, can't include your session token), (2) Public APIs with no authentication required, (3) Mobile apps using API keys or JWT instead of sessions. How to disable safely: @http.route('/webhook/payment', csrf=False) BUT implement alternatives: (a) Signature verification for webhooks (hmac.compare_digest(received_signature, expected_signature) using shared secret), (b) JWT token validation for mobile apps (jwt.decode with secret key), (c) API key authentication, (d) Rate limiting (prevent brute force), (e) IP whitelisting (restrict to known IPs). CRITICAL rule: NEVER use csrf=False without implementing alternative security. Common mistake: Disabling CSRF for convenience without alternatives = massive security hole = accounts hijacked, unauthorized transactions, $200k-$1M damages. Correct pattern: Forms and AJAX use CSRF (default), webhooks use csrf=False + signature verification, mobile APIs use csrf=False + JWT tokens.
How do I include CSRF tokens in AJAX requests?
Get token from page or Odoo core, include in X-CSRF-Token header for all AJAX requests. Method 1 (from page): var csrfToken = document.querySelector('input[name="csrf_token"]').value; reads hidden input auto-embedded by Odoo in forms. Method 2 (from core): var csrfToken = odoo.web.csrf_token; accesses token from Odoo JavaScript core. Include in request: fetch('/api/orders/create', {method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken}, body: JSON.stringify({name: 'Order', amount: 1000})}). jQuery AJAX: $.ajax({url: '/api/orders', type: 'POST', headers: {'X-CSRF-Token': csrfToken}, data: {...}}). Axios: axios.post('/api/orders', data, {headers: {'X-CSRF-Token': csrfToken}}). Server validation: Odoo automatically checks X-CSRF-Token header on all POST/PUT/DELETE requests, rejects if missing or invalid. Common error: Forgetting to include token in AJAX = 400 Bad Request "Session expired" even though session is valid. Solution: Always include X-CSRF-Token header in state-changing AJAX calls.
How do I verify webhook signatures as CSRF alternative?
Use HMAC signature verification with shared secret to validate webhook authenticity when csrf=False. Setup: (1) Payment gateway (Stripe/PayPal) and your server share secret key (WEBHOOK_SECRET environment variable), (2) Gateway calculates signature: HMAC-SHA256(webhook_body, secret), includes in header (X-Webhook-Signature or Stripe-Signature), (3) Your server receives webhook, calculates expected signature using same method, (4) Compares signatures using timing-safe comparison (hmac.compare_digest). Implementation: signature = request.headers.get('X-Webhook-Signature'), body = request.get_data(), secret = os.environ.get('WEBHOOK_SECRET'), expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest(), if not hmac.compare_digest(signature, expected): return 403 Invalid. Why timing-safe: Regular == comparison reveals timing information attackers can exploit, hmac.compare_digest takes constant time regardless of where strings differ. Why it works: Only sender with secret key can generate valid signature, attacker without secret can't fake signature, validates request came from legitimate source. Common patterns: Stripe uses stripe.Webhook.construct_event (built-in verification), PayPal sends signature in PayPal-Transmission-Sig header. Result: Webhook security equivalent to CSRF protection without needing session tokens.
What are the consequences of disabling CSRF without alternatives?
Disabling CSRF without alternative security causes account hijacking, unauthorized transactions, data modification = $200k-$1M damages. Attack scenarios: (1) Unauthorized order deletion (attacker sends form deleting customer orders), (2) Address modification (change delivery to attacker's address), (3) Payment method changes (add attacker's card), (4) Account takeover (change email/password), (5) Fraudulent orders (place orders using victim's account). Real damages: Customer data corrupted, trust destroyed, chargebacks from unauthorized transactions, GDPR violations (unauthorized data access), legal liability, brand reputation damage. Financial impact: $200k-$1M in direct losses (fraudulent transactions, chargebacks), customer lifetime value loss (customers leave after breach), regulatory fines (GDPR €20M or 4% revenue), incident response costs ($50k-$200k forensics and remediation). Why it's preventable: Odoo CSRF protection is automatic (zero cost), alternative security (JWT, signatures) takes 1-2 hours to implement, monitoring unauthorized attempts is straightforward. Pattern we see: Developer disables CSRF because "webhook doesn't work", skips signature verification "for now", forgets to add it back, attacker finds endpoint, exploits it. Prevention: Never use csrf=False without implementing signature verification, JWT tokens, or other strong authentication.
Free CSRF Security Review
Stop risking account hijacking. We'll verify CSRF protection is working, review external API endpoints, implement proper auth (JWT/signatures), test CSRF defenses, and secure webhook integrations. Most D2C brands disable CSRF without proper alternatives. This creates massive security holes costing $100K-$1M in account hijacking and unauthorized transactions.
