Harden WO web UI and fix JS-context XSS in both dashboards

- Use json.dumps for onclick URLs to prevent JS string breakout
- Add .lower() to WO render_badge color lookup matching PO pattern
- Add pagination to get_comments query
- Cap get_work_orders to 500 results matching PO pattern
This commit is contained in:
Adam Moussa 2026-05-12 15:03:05 -04:00
parent 4ba5c37f47
commit 57bcec5a2e
2 changed files with 17 additions and 10 deletions

View file

@ -5,6 +5,7 @@ Serves a simple HTML dashboard for viewing purchase orders.
Accessed via Lambda Function URL. Accessed via Lambda Function URL.
""" """
import json
import os import os
from decimal import Decimal from decimal import Decimal
from html import escape as esc from html import escape as esc
@ -195,7 +196,7 @@ def render_po_list(purchase_orders):
processed = esc((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={esc(json.dumps(f'/po?id={po.get("po_number", "")}'), quote=True)}">
<td style="padding:12px;font-weight:500;color:#3b82f6;">{po_number}</td> <td style="padding:12px;font-weight:500;color:#3b82f6;">{po_number}</td>
<td style="padding:12px;">{supplier}</td> <td style="padding:12px;">{supplier}</td>
<td style="padding:12px;font-weight:500;">{site_code}</td> <td style="padding:12px;font-weight:500;">{site_code}</td>

View file

@ -5,6 +5,7 @@ Serves a simple HTML dashboard for viewing work orders and comments.
Accessed via Lambda Function URL. Accessed via Lambda Function URL.
""" """
import json
import os import os
from html import escape as esc from html import escape as esc
@ -16,7 +17,7 @@ WORK_ORDERS_TABLE = os.environ.get("WORK_ORDERS_TABLE", "WorkOrders")
COMMENTS_TABLE = os.environ.get("COMMENTS_TABLE", "WorkOrderComments") COMMENTS_TABLE = os.environ.get("COMMENTS_TABLE", "WorkOrderComments")
def get_work_orders(): def get_work_orders(limit=500):
table = dynamodb.Table(WORK_ORDERS_TABLE) table = dynamodb.Table(WORK_ORDERS_TABLE)
items = [] items = []
response = table.scan() response = table.scan()
@ -25,16 +26,21 @@ def get_work_orders():
response = table.scan(ExclusiveStartKey=response["LastEvaluatedKey"]) response = table.scan(ExclusiveStartKey=response["LastEvaluatedKey"])
items.extend(response.get("Items", [])) items.extend(response.get("Items", []))
items.sort(key=lambda x: x.get("updated_at", ""), reverse=True) items.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
return items return items[:limit]
def get_comments(work_order_id): def get_comments(work_order_id):
table = dynamodb.Table(COMMENTS_TABLE) table = dynamodb.Table(COMMENTS_TABLE)
response = table.query( items = []
KeyConditionExpression="work_order_id = :woid", kwargs = {
ExpressionAttributeValues={":woid": work_order_id}, "KeyConditionExpression": "work_order_id = :woid",
) "ExpressionAttributeValues": {":woid": work_order_id},
items = response.get("Items", []) }
response = table.query(**kwargs)
items.extend(response.get("Items", []))
while "LastEvaluatedKey" in response:
response = table.query(**kwargs, ExclusiveStartKey=response["LastEvaluatedKey"])
items.extend(response.get("Items", []))
items.sort(key=lambda x: x.get("created_at", ""), reverse=True) items.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return items return items
@ -42,7 +48,7 @@ def get_comments(work_order_id):
def render_badge(value, color_map): def render_badge(value, color_map):
if not value: if not value:
value = "unknown" value = "unknown"
color = color_map.get(value, "#9ca3af") color = color_map.get(value.lower(), "#9ca3af")
label = esc(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>'
@ -168,7 +174,7 @@ def render_work_orders_list(work_orders):
updated = esc((wo.get("updated_at") or "")[:16]) updated = esc((wo.get("updated_at") or "")[: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={esc(json.dumps(f'/wo?id={wo.get("work_order_id", "")}'), quote=True)}">
<td style="padding:12px;font-weight:500;color:#3b82f6;">{wo_id}</td> <td style="padding:12px;font-weight:500;color:#3b82f6;">{wo_id}</td>
<td style="padding:12px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{desc}</td> <td style="padding:12px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{desc}</td>
<td style="padding:12px;">{site}</td> <td style="padding:12px;">{site}</td>