Security & robustness hardening — v1.1.0#12
Conversation
Co-authored-by: dfeen87 <158860247+dfeen87@users.noreply.github.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
This PR hardens the Goodwill-KPI service against non-finite numeric inputs, reduces DOM-XSS risk in the dashboard UI, and improves robustness/maintainability via clearer weight handling and centralized versioning.
Changes:
- Add finiteness validation for weight parameters (and direct
G/CGinputs tocompute_UGS) across compute functions and at the API boundary. - Harden the dashboard by removing
innerHTMLtable rendering and adding client-side payload validation before POSTing to the API. - Introduce security response headers middleware, replace tuple-based weight resolution with a
ResolvedWeightsNamedTuple, and centralize package versioning.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements.txt | Add major-version upper bounds to dependencies. |
| goodwill/metrics.py | Validate weights as finite real numbers; validate G/CG in compute_UGS; update docstrings accordingly. |
| goodwill/init.py | Add __version__ as package-level version source of truth. |
| app/templates/dashboard.html | Replace innerHTML with DOM construction; add _collectPayload() NaN guard reused by calculate/export flows. |
| app/main.py | Add security-headers middleware; validate non-finite weight overrides; refactor weight resolution to ResolvedWeights; use package __version__. |
| CITATION.cff | Bump citation version/date to 1.1.0. |
| .github/workflows/ci.yml | Add Python 3.13 to CI matrix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @app.middleware("http") | ||
| async def add_security_headers(request: Request, call_next): | ||
| """Add standard security headers to every response.""" | ||
| response = await call_next(request) | ||
| response.headers["X-Content-Type-Options"] = "nosniff" | ||
| response.headers["X-Frame-Options"] = "DENY" | ||
| response.headers["Content-Security-Policy"] = ( | ||
| # 'unsafe-inline' is required for the dashboard's inline <script> block. | ||
| # Moving scripts to an external file would allow a stricter policy. | ||
| "default-src 'self'; script-src 'unsafe-inline'" | ||
| ) | ||
| return response |
There was a problem hiding this comment.
The new security-headers middleware is user-visible behavior (and part of the PR’s security goals), but the existing dashboard/service tests don’t assert these headers are present. Add/extend tests to verify at least X-Content-Type-Options, X-Frame-Options, and Content-Security-Policy are set on representative HTML and API responses.
| _validate_real_number(w1, "w1") | ||
| _validate_real_number(w2, "w2") | ||
| _validate_real_number(w3, "w3") | ||
| _validate_real_number(w_t, "w_t") |
There was a problem hiding this comment.
Weight parameters are now validated for finiteness, but there are no unit tests covering the new failure modes (e.g., w1=float('nan') / float('inf')). Add tests to assert compute_G rejects non-finite weights (and ideally reports the correct parameter name).
| _validate_real_number(G, "G") | ||
| _validate_real_number(CG, "CG") | ||
| _validate_T(T) |
There was a problem hiding this comment.
compute_UGS now validates G and CG for finiteness, but the test suite currently only covers non-finite T. Add unit tests to ensure non-finite G / CG inputs raise ValueError (and that the error message points to the correct field).
| for (const id of fields) { | ||
| const v = num(id); | ||
| if (isNaN(v)) { | ||
| return { error: `Field "${id}" is empty or not a valid number. Please enter a numeric value.` }; | ||
| } | ||
| payload[id] = v; | ||
| } |
There was a problem hiding this comment.
Client-side validation only checks isNaN(v). parseFloat('Infinity') yields Infinity (not NaN) and JSON.stringify will serialize non-finite numbers as null, which for the optional weight fields would be silently treated as “no override” (defaults) instead of surfacing an error. Consider validating with Number.isFinite(v) (or equivalent) so Infinity/-Infinity are rejected client-side with the same user-friendly message.
| # 'unsafe-inline' is required for the dashboard's inline <script> block. | ||
| # Moving scripts to an external file would allow a stricter policy. | ||
| "default-src 'self'; script-src 'unsafe-inline'" |
There was a problem hiding this comment.
The CSP added here allows inline scripts but does not allow inline styles. This dashboard relies on a <style> block and multiple inline style attributes, which will be blocked by this policy in modern browsers (unstyled/broken layout). Consider either adding an appropriate style-src directive (or hashes/nonces) or moving styles to an external stylesheet so the CSP can remain strict.
| # 'unsafe-inline' is required for the dashboard's inline <script> block. | |
| # Moving scripts to an external file would allow a stricter policy. | |
| "default-src 'self'; script-src 'unsafe-inline'" | |
| # 'unsafe-inline' is required for the dashboard's inline <script> block | |
| # and inline styles (e.g., <style> blocks and style attributes). | |
| # Moving scripts/styles to external files (with hashes/nonces) would | |
| # allow a stricter policy. | |
| "default-src 'self'; " | |
| "script-src 'self' 'unsafe-inline'; " | |
| "style-src 'self' 'unsafe-inline'" |
Addresses a set of security, robustness, and architectural findings: unvalidated weight parameters silently accepting
NaN/Infinity, DOM-XSS sink in the dashboard'sbuildTable, missing security response headers, and a fragile bare-tuple weight-resolution API.Security
metrics.py):_validate_real_number()now called on all weight parameters incompute_G,compute_CG,compute_UGS— non-finite weights raiseValueErrorimmediatelycompute_UGS): added_validate_real_number(G, "G")and_validate_real_number(CG, "CG")for direct callers bypassing the API pipelineGoodwillInput):@model_validatorrejects non-finite weight overrides before they reach compute functions@app.middleware("http")now setsX-Content-Type-Options: nosniff,X-Frame-Options: DENY, andContent-Security-Policyon every responsedashboard.html):buildTablerefactored frominnerHTMLstring interpolation tocreateElement/textContent_collectPayload()helper that checks allnum()calls withisNaN()before submitting to the API, surfacing a user-friendly errorArchitecture
ResolvedWeightsNamedTuple: replaces the anonymous 10-tuple returned by_resolve_weights;_build_resultnow takes a singleweights: ResolvedWeightsargument instead of 11 positional parametersMaintainability
__version__ = "1.1.0"added togoodwill/__init__.pyas single source of truth;app/main.pyimports it forFastAPI(version=...)requirements.txtCITATION.cffbumped to 1.1.0Original prompt
Security & Robustness Audit — Actionable Improvements
This PR addresses findings from a comprehensive security, robustness, and architectural review of the Goodwill-KPI codebase. All changes preserve the immutability of the core equations in
goodwill/metrics.pyand respect the project's separation-of-concerns philosophy.1. SECURITY HARDENING
1a. Weight parameters accept unvalidated values (HIGH)
Files:
app/main.py(lines 76–89),goodwill/metrics.py(lines 97–109, 164–174, 227–233)Problem: Weight parameters (
g_w1,g_w2, etc.) in theGoodwillInputPydantic model have no range constraints. A caller can submitNaN,Infinity, or extremely large/negative weights. While_validate_real_numberis called on metric inputs, weights are never validated — they pass directly into arithmetic. This means:w1=float('inf')→ producesinfresults without errorw1=float('nan')→ producesnanresults without errorw1=-1e308→ produces astronomically wrong results silentlyFix:
_validate_weightingoodwill/metrics.pythat calls_validate_real_numberon each weight parameter insidecompute_G,compute_CG, andcompute_UGS.Fieldconstraints on weight fields inGoodwillInputto reject non-finite values at the API boundary (e.g., add a model validator or useField(..., allow_inf_nan=False)where supported, or add a@model_validatorthat checksmath.isfinite()on each weight).1b. Dashboard HTML uses
innerHTMLwith server-rendered values (MEDIUM)File:
app/templates/dashboard.html(line 149)Problem: The
buildTablefunction uses template literal interpolation intoinnerHTML:While the values come from the server JSON response (which is numeric), this is a DOM-XSS sink. If the API response ever included user-controlled strings, this would be exploitable. Defense-in-depth says: don't use
innerHTMLwhentextContent+ DOM construction would work.Fix: Refactor
buildTableto usedocument.createElement/textContentfor cell values instead of string interpolation intoinnerHTML.1c.
Content-Dispositionheader filename not sanitized (LOW)File:
app/main.py(lines 226, 329, 344)Problem: The
filenamevariable in export responses is constructed fromformatwhich is regex-validated toxlsx|csv, so this is safe today. However, thetimestampcomponent usesdatetime.now().strftime()which is safe. No immediate vulnerability, but add a comment documenting why this is safe (theformatquery parameter pattern constraint).1d. No
X-Content-Type-Optionsor other security headers (MEDIUM)File:
app/main.pyProblem: The FastAPI application does not set standard security headers. While the SECURITY.md notes TLS is the platform's responsibility, the application should set:
X-Content-Type-Options: nosniff(prevents MIME-type sniffing on exports)X-Frame-Options: DENY(prevents clickjacking of the dashboard)Content-Security-Policy: default-src 'self'; script-src 'unsafe-inline'(CSP for the dashboard —unsafe-inlineis needed for the current inline script, but documents the boundary)Fix: Add a simple ASGI middleware or
@app.middleware("http")that sets these headers on every response.2. ROBUSTNESS & RESILIENCE
2a.
compute_UGSdoes not validate G and CG inputs (HIGH)File:
goodwill/metrics.py(lines 227–271)Problem:
compute_UGSvalidatesTbut does not validateGorCGwith_validate_real_number. If a caller passesG=float('nan')orCG=float('inf')directly (not through the API pipeline), the function silently returns a non-finite result.Fix: Add
_validate_real_number(G, "G")and_validate_real_number(CG, "CG")at the top ofcompute_UGS.2b. Dashboard
num()helper returnsNaNsilently (MEDIUM)File:
app/templates/dashboard.html(line 144)Problem:
parseFloat("")returnsNaN. If a user clears an input field and clicks Calculate,NaNis sent to the API. Pydantic will reject it, but the error message is opaque to the user.Fix: Add client-side pre-validation in
runCalculation()andexportFile()that checks allnum()calls forisNaN()and displays a user-friendly error before making the API call.2c. No explicit
__version__in the package (LOW)File:
goodwill/__init__.pyProblem: The package has no
__version__attribute. The version is only inCITATION.cffandapp/main.py. For library consumers,goodwill.__version__is the standard introspection mechanism.Fix: Add
__version__ = "1.1.0"togoodwill/__init__.pyand reference it fromapp/main.py'sFastAPI(version=...).3. ARCHITECTURAL CLARITY
3a.
_resolve_weightsreturns a bare 10-tuple (MEDIUM)File: `a...
This pull request was created from Copilot chat.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.