Skip to content

Commit 6f25405

Browse files
fix(spp_audit,spp_change_request_v2): add HTML escaping to computed Html fields
Escape dynamic values with markupsafe.escape() before inserting into f-string HTML to prevent stored XSS in audit logs, change request previews, and registrant summaries. Fixes #50
1 parent b99d82c commit 6f25405

3 files changed

Lines changed: 20 additions & 16 deletions

File tree

spp_audit/models/spp_audit_log.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33

44
from dateutil import tz
5+
from markupsafe import escape as html_escape
56

67
from odoo import _, api, fields, models
78
from odoo.exceptions import UserError
@@ -164,7 +165,7 @@ def _compute_data_html(self):
164165
for line in rec._get_content():
165166
row = ""
166167
for item in line:
167-
row += f"<td>{item}</td>"
168+
row += f"<td>{html_escape(str(item))}</td>"
168169
tbody += f"<tr>{row}</tr>"
169170
tbody = f"<tbody>{tbody}</tbody>"
170171
rec.data_html = f'<table class="o_list_view table table-condensed table-striped">{thead}{tbody}</table>'
@@ -206,7 +207,7 @@ def _compute_parent_data_html(self):
206207
for line in rec._parent_get_content():
207208
row = ""
208209
for item in line:
209-
row += f"<td>{item}</td>"
210+
row += f"<td>{html_escape(str(item))}</td>"
210211
tbody += f"<tr>{row}</tr>"
211212
tbody = f"<tbody>{tbody}</tbody>"
212213
rec.parent_data_html = (

spp_change_request_v2/models/change_request.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -541,19 +541,20 @@ def _compute_registrant_summary_html(self):
541541
html_parts.append('<i class="fa fa-users fa-lg text-primary me-2"></i>')
542542
else:
543543
html_parts.append('<i class="fa fa-user fa-lg text-primary me-2"></i>')
544-
html_parts.append(f"<strong>{reg.name}</strong>")
544+
html_parts.append(f"<strong>{html_escape(reg.name or '')}</strong>")
545545
html_parts.append("</div>")
546546

547547
# ID badge
548548
if hasattr(reg, "spp_id") and reg.spp_id:
549-
html_parts.append(f'<div class="mb-2"><span class="badge bg-secondary">ID: {reg.spp_id}</span></div>')
549+
escaped_id = html_escape(reg.spp_id)
550+
html_parts.append(f'<div class="mb-2"><span class="badge bg-secondary">ID: {escaped_id}</span></div>')
550551

551552
# Address
552553
address_parts = []
553554
if reg.street:
554-
address_parts.append(reg.street)
555+
address_parts.append(html_escape(reg.street))
555556
if reg.city:
556-
address_parts.append(reg.city)
557+
address_parts.append(html_escape(reg.city))
557558
if address_parts:
558559
html_parts.append(
559560
f'<div class="text-muted small mb-2">'
@@ -1073,7 +1074,7 @@ def _generate_preview_html(self):
10731074
action_label = action_labels.get(action, action.replace("_", " ").title())
10741075
html_parts.append(
10751076
f'<div class="mb-3 d-flex align-items-center">'
1076-
f'<span class="badge bg-primary me-2">{action_label}</span>'
1077+
f'<span class="badge bg-primary me-2">{html_escape(action_label)}</span>'
10771078
f"</div>"
10781079
)
10791080

@@ -1085,7 +1086,7 @@ def _generate_preview_html(self):
10851086
for key, value in changes.items():
10861087
if key.startswith("_"):
10871088
continue
1088-
display_key = key.replace("_", " ").title()
1089+
display_key = html_escape(key.replace("_", " ").title())
10891090

10901091
# Handle dict with old/new structure
10911092
if isinstance(value, dict) and "new" in value:
@@ -1095,16 +1096,16 @@ def _generate_preview_html(self):
10951096
if old_val is None or old_val is False or old_val == "":
10961097
old_display = '<span class="text-muted">—</span>'
10971098
else:
1098-
old_display = str(old_val)
1099+
old_display = html_escape(str(old_val))
10991100
# Format new value
11001101
if new_val is None or new_val is False or new_val == "":
11011102
new_display = '<span class="text-muted">—</span>'
11021103
else:
1103-
new_display = f"<strong>{new_val}</strong>"
1104+
new_display = f"<strong>{html_escape(str(new_val))}</strong>"
11041105
display_value = f"{old_display}{new_display}"
11051106
elif isinstance(value, list):
11061107
if value:
1107-
display_value = "<br/>".join(str(v) for v in value)
1108+
display_value = "<br/>".join(html_escape(str(v)) for v in value)
11081109
else:
11091110
display_value = '<span class="text-muted">Not set</span>'
11101111
elif value is None or value is False or value == "":
@@ -1114,7 +1115,7 @@ def _generate_preview_html(self):
11141115
# Only True reaches here (False caught above)
11151116
display_value = '<span class="badge text-bg-success">Yes</span>'
11161117
else:
1117-
display_value = str(value)
1118+
display_value = html_escape(str(value))
11181119

11191120
html_parts.append(f"<tr><td><strong>{display_key}</strong></td><td>{display_value}</td></tr>")
11201121

spp_change_request_v2/wizards/preview_changes_wizard.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import json
55

6+
from markupsafe import escape as html_escape
7+
68
from odoo import api, fields, models
79

810

@@ -61,7 +63,7 @@ def _compute_preview_html(self):
6163
html_parts = ['<div class="o_preview_changes">']
6264

6365
action = changes.pop("_action", "update")
64-
html_parts.append(f'<p class="mb-3"><strong>Action:</strong> {action}</p>')
66+
html_parts.append(f'<p class="mb-3"><strong>Action:</strong> {html_escape(str(action))}</p>')
6567

6668
if changes:
6769
html_parts.append('<table class="table table-sm table-striped">')
@@ -70,10 +72,10 @@ def _compute_preview_html(self):
7072

7173
for key, value in changes.items():
7274
# Format the key
73-
display_key = key.replace("_", " ").title()
75+
display_key = html_escape(key.replace("_", " ").title())
7476
# Format the value
7577
if isinstance(value, list):
76-
display_value = "<br/>".join(str(v) for v in value)
78+
display_value = "<br/>".join(html_escape(str(v)) for v in value)
7779
elif value is None:
7880
display_value = '<span class="text-muted">Not set</span>'
7981
elif isinstance(value, bool):
@@ -83,7 +85,7 @@ def _compute_preview_html(self):
8385
else '<span class="badge text-bg-secondary">No</span>'
8486
)
8587
else:
86-
display_value = str(value)
88+
display_value = html_escape(str(value))
8789

8890
html_parts.append(f"<tr><td><strong>{display_key}</strong></td><td>{display_value}</td></tr>")
8991

0 commit comments

Comments
 (0)