mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-01 15:53:13 +00:00
Escape HTML in both web UI dashboards to prevent XSS
Both Function URLs are public (auth_type=NONE) and render email-derived content via f-strings. Attacker-crafted emails could inject scripts. Added html.escape() on all interpolated values in both PO and WO dashboards.
This commit is contained in:
parent
d663983fb1
commit
78eb8de067
2 changed files with 58 additions and 55 deletions
|
|
@ -7,6 +7,7 @@ Accessed via Lambda Function URL.
|
|||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from html import escape as esc
|
||||
|
||||
import boto3
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ def render_badge(value, color_map):
|
|||
if not value:
|
||||
value = "unknown"
|
||||
color = color_map.get(value.lower(), "#9ca3af")
|
||||
label = value.replace("_", " ").title()
|
||||
label = esc(value.replace("_", " ").title())
|
||||
return f'<span style="background:{color};color:#fff;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:500;">{label}</span>'
|
||||
|
||||
|
||||
|
|
@ -62,50 +63,51 @@ def fmt_currency(val):
|
|||
|
||||
|
||||
def render_po_detail(po):
|
||||
po_number = po.get("po_number", "")
|
||||
po_number = esc(po.get("po_number", ""))
|
||||
|
||||
fields = [
|
||||
("PO Number", po_number),
|
||||
("Status", render_badge(po.get("po_status", ""), STATUS_COLORS)),
|
||||
("Email Type", render_badge(po.get("email_type", ""), EMAIL_TYPE_COLORS)),
|
||||
("Total Amount", fmt_currency(po.get("total_amount"))),
|
||||
("Currency", po.get("currency")),
|
||||
("Supplier", (po.get("supplier") or {}).get("name")),
|
||||
("Site Code", po.get("site_code")),
|
||||
("State", po.get("state")),
|
||||
("Trade", po.get("trade")),
|
||||
("Fiscal Year", po.get("fiscal_year")),
|
||||
("Coupa Category", po.get("coupa_category")),
|
||||
("Submitted By", po.get("submitted_by")),
|
||||
("On Behalf Of", po.get("on_behalf_of")),
|
||||
("Order Date", po.get("order_date")),
|
||||
("Revision Date", po.get("revision_date")),
|
||||
("Payment Terms", po.get("payment_terms")),
|
||||
("Requisition #", po.get("requisition_number")),
|
||||
("Department", po.get("department")),
|
||||
("Data Source", po.get("data_source")),
|
||||
("Processed At", po.get("processed_at")),
|
||||
("Currency", esc(po.get("currency", "")) or None),
|
||||
("Supplier", esc((po.get("supplier") or {}).get("name", "")) or None),
|
||||
("Site Code", esc(po.get("site_code", "")) or None),
|
||||
("State", esc(po.get("state", "")) or None),
|
||||
("Trade", esc(po.get("trade", "")) or None),
|
||||
("Fiscal Year", esc(po.get("fiscal_year", "")) or None),
|
||||
("Coupa Category", esc(po.get("coupa_category", "")) or None),
|
||||
("Submitted By", esc(po.get("submitted_by", "")) or None),
|
||||
("On Behalf Of", esc(po.get("on_behalf_of", "")) or None),
|
||||
("Order Date", esc(po.get("order_date", "")) or None),
|
||||
("Revision Date", esc(po.get("revision_date", "")) or None),
|
||||
("Payment Terms", esc(po.get("payment_terms", "")) or None),
|
||||
("Requisition #", esc(po.get("requisition_number", "")) or None),
|
||||
("Department", esc(po.get("department", "")) or None),
|
||||
("Data Source", esc(po.get("data_source", "")) or None),
|
||||
("Processed At", esc(po.get("processed_at", "")) or None),
|
||||
]
|
||||
|
||||
ship_to = po.get("ship_to") or {}
|
||||
if any(ship_to.values()):
|
||||
ship_parts = []
|
||||
if ship_to.get("name"):
|
||||
ship_parts.append(ship_to["name"])
|
||||
ship_parts.append(esc(ship_to["name"]))
|
||||
if ship_to.get("address"):
|
||||
ship_parts.append(ship_to["address"])
|
||||
ship_parts.append(esc(ship_to["address"]))
|
||||
if ship_to.get("location_code"):
|
||||
ship_parts.append(f"Location: {ship_to['location_code']}")
|
||||
ship_parts.append(f"Location: {esc(ship_to['location_code'])}")
|
||||
if ship_to.get("attn"):
|
||||
ship_parts.append(f"Attn: {ship_to['attn']}")
|
||||
ship_parts.append(f"Attn: {esc(ship_to['attn'])}")
|
||||
fields.append(("Ship To", "<br>".join(ship_parts)))
|
||||
|
||||
view_url = po.get("view_order_url")
|
||||
if view_url:
|
||||
escaped_url = esc(view_url, quote=True)
|
||||
fields.append(
|
||||
(
|
||||
"Coupa Link",
|
||||
f'<a href="{view_url}" target="_blank" style="color:#3b82f6;">View in Coupa</a>',
|
||||
f'<a href="{escaped_url}" target="_blank" style="color:#3b82f6;">View in Coupa</a>',
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -124,17 +126,17 @@ def render_po_detail(po):
|
|||
if line_items:
|
||||
rows = ""
|
||||
for item in line_items:
|
||||
qty = item.get("quantity", "") or ""
|
||||
unit = item.get("unit", "") or ""
|
||||
price = item.get("price", "") or ""
|
||||
qty = esc(str(item.get("quantity", "") or ""))
|
||||
unit = esc(str(item.get("unit", "") or ""))
|
||||
price = esc(str(item.get("price", "") or ""))
|
||||
rows += f"""
|
||||
<tr style="border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:10px;font-size:14px;">{item.get("description", "")}</td>
|
||||
<td style="padding:10px;font-size:14px;">{esc(str(item.get("description", "")))}</td>
|
||||
<td style="padding:10px;font-size:13px;text-align:right;">{qty}</td>
|
||||
<td style="padding:10px;font-size:13px;">{unit}</td>
|
||||
<td style="padding:10px;font-size:13px;text-align:right;">{price}</td>
|
||||
<td style="padding:10px;font-size:14px;text-align:right;">{fmt_currency(item.get("amount"))}</td>
|
||||
<td style="padding:10px;font-size:13px;color:#64748b;">{item.get("need_by", "") or ""}</td>
|
||||
<td style="padding:10px;font-size:13px;color:#64748b;">{esc(str(item.get("need_by", "") or ""))}</td>
|
||||
</tr>"""
|
||||
|
||||
items_html = f"""
|
||||
|
|
@ -184,13 +186,13 @@ def render_po_detail(po):
|
|||
def render_po_list(purchase_orders):
|
||||
rows = ""
|
||||
for po in purchase_orders:
|
||||
po_number = po.get("po_number", "")
|
||||
supplier = (po.get("supplier") or {}).get("name", "")
|
||||
site_code = po.get("site_code", "")
|
||||
trade = po.get("trade", "")
|
||||
po_number = esc(po.get("po_number", ""))
|
||||
supplier = esc((po.get("supplier") or {}).get("name", ""))
|
||||
site_code = esc(po.get("site_code", ""))
|
||||
trade = esc(po.get("trade", ""))
|
||||
status = po.get("po_status", "")
|
||||
total = fmt_currency(po.get("total_amount"))
|
||||
processed = (po.get("processed_at") or "")[:16]
|
||||
processed = esc((po.get("processed_at") or "")[:16])
|
||||
|
||||
rows += f"""
|
||||
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/po?id={po_number}'">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Accessed via Lambda Function URL.
|
|||
"""
|
||||
|
||||
import os
|
||||
from html import escape as esc
|
||||
|
||||
import boto3
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ def get_comments(work_order_id):
|
|||
|
||||
def render_badge(value, color_map):
|
||||
color = color_map.get(value, "#9ca3af")
|
||||
label = value.replace("_", " ").title()
|
||||
label = esc(value.replace("_", " ").title())
|
||||
return f'<span style="background:{color};color:#fff;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:500;">{label}</span>'
|
||||
|
||||
|
||||
|
|
@ -65,9 +66,9 @@ def render_work_order_detail(wo, events):
|
|||
if events:
|
||||
for e in events:
|
||||
record_type = e.get("record_type", "unknown")
|
||||
commenter = e.get("commenter", "")
|
||||
created = e.get("created_at", "")
|
||||
text = e.get("text", "")
|
||||
commenter = esc(e.get("commenter", ""))
|
||||
created = esc(e.get("created_at", ""))
|
||||
text = esc(e.get("text", ""))
|
||||
badge = render_badge(record_type, RECORD_TYPE_COLORS)
|
||||
commenter_str = (
|
||||
f"<strong>{commenter}</strong> — " if commenter else ""
|
||||
|
|
@ -91,25 +92,25 @@ def render_work_order_detail(wo, events):
|
|||
else:
|
||||
events_html = '<p style="color:#94a3b8;font-style:italic;">No events yet.</p>'
|
||||
|
||||
wo_id = wo.get("work_order_id", "")
|
||||
wo_id = esc(wo.get("work_order_id", ""))
|
||||
fields = [
|
||||
("Description", wo.get("description")),
|
||||
("Description", esc(wo.get("description", "")) or None),
|
||||
("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)),
|
||||
(
|
||||
"Record Type",
|
||||
render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS),
|
||||
),
|
||||
("Site Code", wo.get("site_code")),
|
||||
("Building", wo.get("building")),
|
||||
("Address", wo.get("address")),
|
||||
("Severity", wo.get("severity")),
|
||||
("Priority", wo.get("priority")),
|
||||
("Due Date", wo.get("due_date")),
|
||||
("Date Reported", wo.get("date_reported")),
|
||||
("Scheduled Start", wo.get("scheduled_start")),
|
||||
("Assigned To", wo.get("assigned_to")),
|
||||
("Created", wo.get("created_at")),
|
||||
("Last Updated", wo.get("updated_at")),
|
||||
("Site Code", esc(wo.get("site_code", "")) or None),
|
||||
("Building", esc(wo.get("building", "")) or None),
|
||||
("Address", esc(wo.get("address", "")) or None),
|
||||
("Severity", esc(wo.get("severity", "")) or None),
|
||||
("Priority", esc(wo.get("priority", "")) or None),
|
||||
("Due Date", esc(wo.get("due_date", "")) or None),
|
||||
("Date Reported", esc(wo.get("date_reported", "")) or None),
|
||||
("Scheduled Start", esc(wo.get("scheduled_start", "")) or None),
|
||||
("Assigned To", esc(wo.get("assigned_to", "")) or None),
|
||||
("Created", esc(wo.get("created_at", "")) or None),
|
||||
("Last Updated", esc(wo.get("updated_at", "")) or None),
|
||||
]
|
||||
|
||||
details_html = ""
|
||||
|
|
@ -153,13 +154,13 @@ def render_work_order_detail(wo, events):
|
|||
def render_work_orders_list(work_orders):
|
||||
rows = ""
|
||||
for wo in work_orders:
|
||||
wo_id = wo.get("work_order_id", "")
|
||||
desc = wo.get("description", "")
|
||||
site = wo.get("site_code", "")
|
||||
wo_id = esc(wo.get("work_order_id", ""))
|
||||
desc = esc(wo.get("description", ""))
|
||||
site = esc(wo.get("site_code", ""))
|
||||
status = wo.get("wo_status", "unknown")
|
||||
record_type = wo.get("record_type", "unknown")
|
||||
due = wo.get("due_date", "")
|
||||
updated = wo.get("updated_at", "")[:16]
|
||||
due = esc(wo.get("due_date", ""))
|
||||
updated = esc(wo.get("updated_at", "")[:16])
|
||||
|
||||
rows += f"""
|
||||
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/wo?id={wo_id}'">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue