mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-03 04:33: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
|
import os
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from html import escape as esc
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ def render_badge(value, color_map):
|
||||||
if not value:
|
if not value:
|
||||||
value = "unknown"
|
value = "unknown"
|
||||||
color = color_map.get(value.lower(), "#9ca3af")
|
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>'
|
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):
|
def render_po_detail(po):
|
||||||
po_number = po.get("po_number", "")
|
po_number = esc(po.get("po_number", ""))
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
("PO Number", po_number),
|
("PO Number", po_number),
|
||||||
("Status", render_badge(po.get("po_status", ""), STATUS_COLORS)),
|
("Status", render_badge(po.get("po_status", ""), STATUS_COLORS)),
|
||||||
("Email Type", render_badge(po.get("email_type", ""), EMAIL_TYPE_COLORS)),
|
("Email Type", render_badge(po.get("email_type", ""), EMAIL_TYPE_COLORS)),
|
||||||
("Total Amount", fmt_currency(po.get("total_amount"))),
|
("Total Amount", fmt_currency(po.get("total_amount"))),
|
||||||
("Currency", po.get("currency")),
|
("Currency", esc(po.get("currency", "")) or None),
|
||||||
("Supplier", (po.get("supplier") or {}).get("name")),
|
("Supplier", esc((po.get("supplier") or {}).get("name", "")) or None),
|
||||||
("Site Code", po.get("site_code")),
|
("Site Code", esc(po.get("site_code", "")) or None),
|
||||||
("State", po.get("state")),
|
("State", esc(po.get("state", "")) or None),
|
||||||
("Trade", po.get("trade")),
|
("Trade", esc(po.get("trade", "")) or None),
|
||||||
("Fiscal Year", po.get("fiscal_year")),
|
("Fiscal Year", esc(po.get("fiscal_year", "")) or None),
|
||||||
("Coupa Category", po.get("coupa_category")),
|
("Coupa Category", esc(po.get("coupa_category", "")) or None),
|
||||||
("Submitted By", po.get("submitted_by")),
|
("Submitted By", esc(po.get("submitted_by", "")) or None),
|
||||||
("On Behalf Of", po.get("on_behalf_of")),
|
("On Behalf Of", esc(po.get("on_behalf_of", "")) or None),
|
||||||
("Order Date", po.get("order_date")),
|
("Order Date", esc(po.get("order_date", "")) or None),
|
||||||
("Revision Date", po.get("revision_date")),
|
("Revision Date", esc(po.get("revision_date", "")) or None),
|
||||||
("Payment Terms", po.get("payment_terms")),
|
("Payment Terms", esc(po.get("payment_terms", "")) or None),
|
||||||
("Requisition #", po.get("requisition_number")),
|
("Requisition #", esc(po.get("requisition_number", "")) or None),
|
||||||
("Department", po.get("department")),
|
("Department", esc(po.get("department", "")) or None),
|
||||||
("Data Source", po.get("data_source")),
|
("Data Source", esc(po.get("data_source", "")) or None),
|
||||||
("Processed At", po.get("processed_at")),
|
("Processed At", esc(po.get("processed_at", "")) or None),
|
||||||
]
|
]
|
||||||
|
|
||||||
ship_to = po.get("ship_to") or {}
|
ship_to = po.get("ship_to") or {}
|
||||||
if any(ship_to.values()):
|
if any(ship_to.values()):
|
||||||
ship_parts = []
|
ship_parts = []
|
||||||
if ship_to.get("name"):
|
if ship_to.get("name"):
|
||||||
ship_parts.append(ship_to["name"])
|
ship_parts.append(esc(ship_to["name"]))
|
||||||
if ship_to.get("address"):
|
if ship_to.get("address"):
|
||||||
ship_parts.append(ship_to["address"])
|
ship_parts.append(esc(ship_to["address"]))
|
||||||
if ship_to.get("location_code"):
|
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"):
|
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)))
|
fields.append(("Ship To", "<br>".join(ship_parts)))
|
||||||
|
|
||||||
view_url = po.get("view_order_url")
|
view_url = po.get("view_order_url")
|
||||||
if view_url:
|
if view_url:
|
||||||
|
escaped_url = esc(view_url, quote=True)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
"Coupa Link",
|
"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:
|
if line_items:
|
||||||
rows = ""
|
rows = ""
|
||||||
for item in line_items:
|
for item in line_items:
|
||||||
qty = item.get("quantity", "") or ""
|
qty = esc(str(item.get("quantity", "") or ""))
|
||||||
unit = item.get("unit", "") or ""
|
unit = esc(str(item.get("unit", "") or ""))
|
||||||
price = item.get("price", "") or ""
|
price = esc(str(item.get("price", "") or ""))
|
||||||
rows += f"""
|
rows += f"""
|
||||||
<tr style="border-bottom:1px solid #f1f5f9;">
|
<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;text-align:right;">{qty}</td>
|
||||||
<td style="padding:10px;font-size:13px;">{unit}</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: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: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>"""
|
</tr>"""
|
||||||
|
|
||||||
items_html = f"""
|
items_html = f"""
|
||||||
|
|
@ -184,13 +186,13 @@ def render_po_detail(po):
|
||||||
def render_po_list(purchase_orders):
|
def render_po_list(purchase_orders):
|
||||||
rows = ""
|
rows = ""
|
||||||
for po in purchase_orders:
|
for po in purchase_orders:
|
||||||
po_number = po.get("po_number", "")
|
po_number = esc(po.get("po_number", ""))
|
||||||
supplier = (po.get("supplier") or {}).get("name", "")
|
supplier = esc((po.get("supplier") or {}).get("name", ""))
|
||||||
site_code = po.get("site_code", "")
|
site_code = esc(po.get("site_code", ""))
|
||||||
trade = po.get("trade", "")
|
trade = esc(po.get("trade", ""))
|
||||||
status = po.get("po_status", "")
|
status = po.get("po_status", "")
|
||||||
total = fmt_currency(po.get("total_amount"))
|
total = fmt_currency(po.get("total_amount"))
|
||||||
processed = (po.get("processed_at") or "")[:16]
|
processed = esc((po.get("processed_at") or "")[:16])
|
||||||
|
|
||||||
rows += f"""
|
rows += f"""
|
||||||
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/po?id={po_number}'">
|
<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
|
import os
|
||||||
|
from html import escape as esc
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ def get_comments(work_order_id):
|
||||||
|
|
||||||
def render_badge(value, color_map):
|
def render_badge(value, color_map):
|
||||||
color = color_map.get(value, "#9ca3af")
|
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>'
|
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:
|
if events:
|
||||||
for e in events:
|
for e in events:
|
||||||
record_type = e.get("record_type", "unknown")
|
record_type = e.get("record_type", "unknown")
|
||||||
commenter = e.get("commenter", "")
|
commenter = esc(e.get("commenter", ""))
|
||||||
created = e.get("created_at", "")
|
created = esc(e.get("created_at", ""))
|
||||||
text = e.get("text", "")
|
text = esc(e.get("text", ""))
|
||||||
badge = render_badge(record_type, RECORD_TYPE_COLORS)
|
badge = render_badge(record_type, RECORD_TYPE_COLORS)
|
||||||
commenter_str = (
|
commenter_str = (
|
||||||
f"<strong>{commenter}</strong> — " if commenter else ""
|
f"<strong>{commenter}</strong> — " if commenter else ""
|
||||||
|
|
@ -91,25 +92,25 @@ def render_work_order_detail(wo, events):
|
||||||
else:
|
else:
|
||||||
events_html = '<p style="color:#94a3b8;font-style:italic;">No events yet.</p>'
|
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 = [
|
fields = [
|
||||||
("Description", wo.get("description")),
|
("Description", esc(wo.get("description", "")) or None),
|
||||||
("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)),
|
("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)),
|
||||||
(
|
(
|
||||||
"Record Type",
|
"Record Type",
|
||||||
render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS),
|
render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS),
|
||||||
),
|
),
|
||||||
("Site Code", wo.get("site_code")),
|
("Site Code", esc(wo.get("site_code", "")) or None),
|
||||||
("Building", wo.get("building")),
|
("Building", esc(wo.get("building", "")) or None),
|
||||||
("Address", wo.get("address")),
|
("Address", esc(wo.get("address", "")) or None),
|
||||||
("Severity", wo.get("severity")),
|
("Severity", esc(wo.get("severity", "")) or None),
|
||||||
("Priority", wo.get("priority")),
|
("Priority", esc(wo.get("priority", "")) or None),
|
||||||
("Due Date", wo.get("due_date")),
|
("Due Date", esc(wo.get("due_date", "")) or None),
|
||||||
("Date Reported", wo.get("date_reported")),
|
("Date Reported", esc(wo.get("date_reported", "")) or None),
|
||||||
("Scheduled Start", wo.get("scheduled_start")),
|
("Scheduled Start", esc(wo.get("scheduled_start", "")) or None),
|
||||||
("Assigned To", wo.get("assigned_to")),
|
("Assigned To", esc(wo.get("assigned_to", "")) or None),
|
||||||
("Created", wo.get("created_at")),
|
("Created", esc(wo.get("created_at", "")) or None),
|
||||||
("Last Updated", wo.get("updated_at")),
|
("Last Updated", esc(wo.get("updated_at", "")) or None),
|
||||||
]
|
]
|
||||||
|
|
||||||
details_html = ""
|
details_html = ""
|
||||||
|
|
@ -153,13 +154,13 @@ def render_work_order_detail(wo, events):
|
||||||
def render_work_orders_list(work_orders):
|
def render_work_orders_list(work_orders):
|
||||||
rows = ""
|
rows = ""
|
||||||
for wo in work_orders:
|
for wo in work_orders:
|
||||||
wo_id = wo.get("work_order_id", "")
|
wo_id = esc(wo.get("work_order_id", ""))
|
||||||
desc = wo.get("description", "")
|
desc = esc(wo.get("description", ""))
|
||||||
site = wo.get("site_code", "")
|
site = esc(wo.get("site_code", ""))
|
||||||
status = wo.get("wo_status", "unknown")
|
status = wo.get("wo_status", "unknown")
|
||||||
record_type = wo.get("record_type", "unknown")
|
record_type = wo.get("record_type", "unknown")
|
||||||
due = wo.get("due_date", "")
|
due = esc(wo.get("due_date", ""))
|
||||||
updated = wo.get("updated_at", "")[:16]
|
updated = esc(wo.get("updated_at", "")[:16])
|
||||||
|
|
||||||
rows += f"""
|
rows += f"""
|
||||||
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/wo?id={wo_id}'">
|
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/wo?id={wo_id}'">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue