Skip to content

Commit 96492ff

Browse files
planet.jeroenclaude
andcommitted
fix: harden remaining findings — sweep DoS, constraint validation, XSS, JSON safety, CCSDS parser, CLI errors
Sweep: validate step>0, min<=max, cap at 1000 iterations, index-based loop. Constraints: validate required keys, operator whitelist, numeric threshold, lock. JSON: sanitize inf/nan to null in all API responses. HTTP: top-level exception guards on GET/POST/PUT/DELETE returning 500. Export: sanitize Content-Disposition filename. Compare: only compute delta for metrics present on both sides. Evaluate: skip malformed constraint dicts instead of crashing. Frontend: add escapeHtml() utility, apply to all innerHTML renders, fix report URL. CCSDS parser: duplicate key detection, NaN/inf rejection, velocity guard, empty states guard, dead code removal, space-epoch support. CLI: error handling for FileNotFoundError and CcsdsValidationError in CCSDS imports. 3573 tests passing, 5 skipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 410ad87 commit 96492ff

File tree

5 files changed

+487
-77
lines changed

5 files changed

+487
-77
lines changed

packages/core/src/humeris/cli.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,9 @@ def _run_sweep(args) -> None:
414414
output_path = args.output
415415

416416
if fmt == "json":
417-
import json as _json
417+
import json
418418
with open(output_path, "w", encoding="utf-8") as f:
419-
_json.dump(results, f, indent=2, default=str)
419+
json.dump(results, f, indent=2, default=str)
420420
else:
421421
# CSV
422422
if not results:
@@ -446,7 +446,15 @@ def _run_sweep(args) -> None:
446446
def _run_import_opm(args) -> None:
447447
"""Import CCSDS OPM file and display satellite info."""
448448
from humeris.domain.ccsds_parser import parse_opm
449-
result = parse_opm(args.import_opm)
449+
from humeris.domain.ccsds_contracts import CcsdsValidationError
450+
try:
451+
result = parse_opm(args.import_opm)
452+
except FileNotFoundError:
453+
print(f"Error: File not found: {args.import_opm}", file=sys.stderr)
454+
sys.exit(1)
455+
except CcsdsValidationError as e:
456+
print(f"Error: Invalid OPM file: {e}", file=sys.stderr)
457+
sys.exit(1)
450458
print(f"Object: {result.object_name} ({result.object_id})")
451459
print(f"Frame: {result.ref_frame}, Center: {result.center_name}")
452460
for i, state in enumerate(result.states):
@@ -458,7 +466,15 @@ def _run_import_opm(args) -> None:
458466
def _run_import_oem(args) -> None:
459467
"""Import CCSDS OEM file and display satellite info."""
460468
from humeris.domain.ccsds_parser import parse_oem
461-
result = parse_oem(args.import_oem)
469+
from humeris.domain.ccsds_contracts import CcsdsValidationError
470+
try:
471+
result = parse_oem(args.import_oem)
472+
except FileNotFoundError:
473+
print(f"Error: File not found: {args.import_oem}", file=sys.stderr)
474+
sys.exit(1)
475+
except CcsdsValidationError as e:
476+
print(f"Error: Invalid OEM file: {e}", file=sys.stderr)
477+
sys.exit(1)
462478
print(f"Object: {result.object_name} ({result.object_id})")
463479
print(f"Frame: {result.ref_frame}, Center: {result.center_name}")
464480
print(f"Ephemeris points: {len(result.states)}")

packages/core/src/humeris/domain/ccsds_parser.py

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ def _parse_kvn_pairs(text: str) -> list[tuple[str, str]]:
5656

5757
def _parse_epoch(epoch_str: str) -> datetime:
5858
"""Parse CCSDS epoch string to datetime."""
59-
# Handle fractional seconds
60-
for fmt in ("%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"):
59+
for fmt in (
60+
"%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S",
61+
"%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S",
62+
):
6163
try:
6264
dt = datetime.strptime(epoch_str, fmt)
6365
return dt.replace(tzinfo=timezone.utc)
@@ -86,6 +88,8 @@ def _cartesian_to_orbital_state(
8688

8789
if r_mag < 1.0:
8890
raise CcsdsValidationError("Position vector magnitude too small")
91+
if v_mag < 1.0:
92+
raise CcsdsValidationError("Velocity vector magnitude too small")
8993

9094
# Specific angular momentum
9195
h_vec = np.cross(pos, vel)
@@ -175,13 +179,25 @@ def parse_opm(path: str) -> CcsdsOrbitData:
175179
text = f.read()
176180

177181
pairs = _parse_kvn_pairs(text)
178-
kvn = dict(pairs)
179182

180-
# Validate required fields
183+
# Check for duplicate required keys
181184
required = {
182185
"OBJECT_NAME", "OBJECT_ID", "CENTER_NAME", "REF_FRAME",
183186
"TIME_SYSTEM", "EPOCH", "X", "Y", "Z", "X_DOT", "Y_DOT", "Z_DOT",
184187
}
188+
seen: dict[str, int] = {}
189+
for key, _ in pairs:
190+
if key in required:
191+
seen[key] = seen.get(key, 0) + 1
192+
duplicates = sorted(k for k, v in seen.items() if v > 1)
193+
if duplicates:
194+
raise CcsdsValidationError(
195+
f"Duplicate required keys in OPM: {', '.join(duplicates)}"
196+
)
197+
198+
kvn = dict(pairs)
199+
200+
# Validate required fields
185201
missing = sorted(f for f in required if f not in kvn)
186202
if missing:
187203
raise CcsdsValidationError(
@@ -229,39 +245,8 @@ def parse_oem(path: str) -> CcsdsOrbitData:
229245

230246
pairs = _parse_kvn_pairs(text)
231247

232-
# Extract header-level fields
233-
header: dict[str, str] = {}
234-
for key, val in pairs:
235-
if key in ("CCSDS_OEM_VERS", "CREATION_DATE", "ORIGINATOR"):
236-
header[key] = val
237-
238-
# Parse segments (each META_START...META_STOP block + data lines)
248+
# Collect segments
239249
segments: list[tuple[dict[str, str], list[str]]] = []
240-
meta: dict[str, str] = {}
241-
data_lines: list[str] = []
242-
in_meta = False
243-
244-
for key, val in pairs:
245-
if key == "META_START":
246-
in_meta = True
247-
meta = {}
248-
data_lines = []
249-
elif key == "META_STOP":
250-
in_meta = False
251-
elif in_meta:
252-
meta[key] = val
253-
elif key == "_DATA_LINE" and meta:
254-
data_lines.append(val)
255-
elif key == "META_START" or (not in_meta and key == "_DATA_LINE"):
256-
pass
257-
# When we hit the next META_START, save current segment
258-
if key == "META_START" and segments or (key == "META_START" and not segments):
259-
if data_lines and meta:
260-
# Save previous segment before starting new one
261-
pass
262-
263-
# Finalize — collect segments by re-parsing
264-
segments = []
265250
meta = {}
266251
data_lines = []
267252
in_meta = False
@@ -301,17 +286,27 @@ def parse_oem(path: str) -> CcsdsOrbitData:
301286
if len(parts) < 7:
302287
continue
303288
epoch = _parse_epoch(parts[0])
289+
try:
290+
values = [float(parts[i]) for i in range(1, 7)]
291+
except ValueError as e:
292+
raise CcsdsValidationError(
293+
f"Non-numeric value in OEM data line: {e}"
294+
) from e
295+
for i, v in enumerate(values):
296+
if math.isnan(v) or math.isinf(v):
297+
raise CcsdsValidationError(
298+
f"NaN or Inf in OEM data line column {i + 1}"
299+
)
304300
state = _cartesian_to_orbital_state(
305-
x_km=float(parts[1]),
306-
y_km=float(parts[2]),
307-
z_km=float(parts[3]),
308-
vx_kms=float(parts[4]),
309-
vy_kms=float(parts[5]),
310-
vz_kms=float(parts[6]),
301+
x_km=values[0], y_km=values[1], z_km=values[2],
302+
vx_kms=values[3], vy_kms=values[4], vz_kms=values[5],
311303
epoch=epoch,
312304
)
313305
all_states.append(state)
314306

307+
if not all_states:
308+
raise CcsdsValidationError("OEM file contains no valid ephemeris data")
309+
315310
return CcsdsOrbitData(
316311
object_name=object_name,
317312
object_id=object_id,

packages/pro/src/humeris/adapters/cesium_viewer.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,13 @@ def generate_interactive_html(
893893
}}
894894
}}
895895
896+
// --- HTML escaping utility ---
897+
function escapeHtml(str) {{
898+
if (str === null || str === undefined) return "";
899+
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;")
900+
.replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
901+
}}
902+
896903
// --- Satellite data table (APP-03) ---
897904
var satTableData = null;
898905
var satTableSort = {{col: null, asc: true}};
@@ -938,13 +945,13 @@ def generate_interactive_html(
938945
cols.forEach(function(c) {{
939946
var cls = "";
940947
if (satTableSort.col === c) cls = satTableSort.asc ? "sort-asc" : "sort-desc";
941-
html += '<th class="' + cls + '" onclick="sortSatTable(\'' + c + '\')">' + c.replace(/_/g, " ") + '</th>';
948+
html += '<th class="' + cls + '" onclick="sortSatTable(\'' + escapeHtml(c) + '\')">' + escapeHtml(c.replace(/_/g, " ")) + '</th>';
942949
}});
943950
html += '</tr></thead><tbody>';
944951
rows.forEach(function(row, idx) {{
945952
html += '<tr onclick="flyToSat(' + idx + ')">';
946953
cols.forEach(function(c) {{
947-
html += '<td>' + row[c] + '</td>';
954+
html += '<td>' + escapeHtml(row[c]) + '</td>';
948955
}});
949956
html += '</tr>';
950957
}});
@@ -1259,11 +1266,11 @@ def generate_interactive_html(
12591266
el.style.display = "none";
12601267
return;
12611268
}}
1262-
var html = '<div class="legend-title">' + (title || "Legend") + '</div>';
1269+
var html = '<div class="legend-title">' + escapeHtml(title || "Legend") + '</div>';
12631270
legend.forEach(function(entry) {{
12641271
html += '<div class="legend-entry">' +
1265-
'<span class="legend-swatch" style="background:' + entry.color + '"></span>' +
1266-
'<span>' + entry.label + '</span></div>';
1272+
'<span class="legend-swatch" style="background:' + escapeHtml(entry.color) + '"></span>' +
1273+
'<span>' + escapeHtml(entry.label) + '</span></div>';
12671274
}});
12681275
el.innerHTML = html;
12691276
el.style.display = "block";
@@ -1317,16 +1324,16 @@ def generate_interactive_html(
13171324
var html = "";
13181325
recent.forEach(function(r) {{
13191326
var ts = r.timestamp ? new Date(r.timestamp).toLocaleDateString() : "";
1320-
html += '<div class="recent-item" title="' + (r.description || "") + '">';
1321-
html += '<span class="recent-name">' + (r.name || "Untitled") + '</span>';
1322-
html += '<span class="recent-meta">' + r.layer_count + ' layers | ' + ts + '</span>';
1327+
html += '<div class="recent-item" title="' + escapeHtml(r.description || "") + '">';
1328+
html += '<span class="recent-name">' + escapeHtml(r.name || "Untitled") + '</span>';
1329+
html += '<span class="recent-meta">' + escapeHtml(r.layer_count) + ' layers | ' + escapeHtml(ts) + '</span>';
13231330
html += '</div>';
13241331
}});
13251332
container.innerHTML = html;
13261333
}}
13271334
// --- Report (APP-08) ---
13281335
function generateReport() {{
1329-
window.open("/api/report", "_blank");
1336+
window.open(API + "/api/report", "_blank");
13301337
}}
13311338
13321339
// --- Constraints (APP-07) ---
@@ -1389,8 +1396,8 @@ def generate_interactive_html(
13891396
apiPost("/api/compare", {{layer_a: layerA, layer_b: layerB}}).then(function(result) {{
13901397
var html = '<table style="width:100%;border-collapse:collapse">';
13911398
html += '<tr><th style="text-align:left;color:rgba(255,255,255,0.5)">Metric</th>';
1392-
html += '<th style="text-align:right;color:rgba(80,160,255,0.8)">' + result.config_a.name.split(":").pop() + '</th>';
1393-
html += '<th style="text-align:right;color:rgba(80,200,120,0.8)">' + result.config_b.name.split(":").pop() + '</th>';
1399+
html += '<th style="text-align:right;color:rgba(80,160,255,0.8)">' + escapeHtml(result.config_a.name.split(":").pop()) + '</th>';
1400+
html += '<th style="text-align:right;color:rgba(80,200,120,0.8)">' + escapeHtml(result.config_b.name.split(":").pop()) + '</th>';
13941401
html += '<th style="text-align:right;color:rgba(255,200,80,0.8)">Delta</th></tr>';
13951402
var allKeys = Object.keys(result.delta);
13961403
allKeys.forEach(function(k) {{
@@ -1400,7 +1407,7 @@ def generate_interactive_html(
14001407
var dColor = d > 0 ? "rgba(80,200,120,0.9)" : d < 0 ? "rgba(255,100,100,0.9)" : "rgba(255,255,255,0.5)";
14011408
var dStr = d > 0 ? "+" + d.toFixed(2) : d.toFixed(2);
14021409
html += '<tr>';
1403-
html += '<td style="color:rgba(255,255,255,0.6)">' + k.replace(/_/g, " ") + '</td>';
1410+
html += '<td style="color:rgba(255,255,255,0.6)">' + escapeHtml(k.replace(/_/g, " ")) + '</td>';
14041411
html += '<td style="text-align:right;font-family:monospace">' + (va !== undefined ? (typeof va === "number" ? va.toFixed(2) : va) : "-") + '</td>';
14051412
html += '<td style="text-align:right;font-family:monospace">' + (vb !== undefined ? (typeof vb === "number" ? vb.toFixed(2) : vb) : "-") + '</td>';
14061413
html += '<td style="text-align:right;font-family:monospace;color:' + dColor + '">' + dStr + '</td>';

0 commit comments

Comments
 (0)