From 49d08ee97a32877cd0c294850b71a1b08dc40a29 Mon Sep 17 00:00:00 2001 From: "Kumar,Avinash,IN-Bangalore" Date: Wed, 5 Aug 2026 19:48:26 +0530 Subject: [PATCH 1/2] feat: implement main application orchestrator and tab navigation logic in app.js --- js/app.js | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/js/app.js b/js/app.js index b2ffa4e..28f9fdd 100644 --- a/js/app.js +++ b/js/app.js @@ -12,6 +12,14 @@ import { initLearning, renderLearningTab } from './learning.js'; let currentTab = "dashboard"; let jargonSimplified = false; +// Tab Routing Registry +const tabHandlers = { + "dashboard": renderGoals, + "simulator": updateSimulator, + "passport": renderPassportTab, + "learning": renderLearningTab +}; + /** * Handles tab switching logic * @param {string} tabId @@ -39,15 +47,8 @@ function switchTab(tabId) { }); // Module specific updates when switching - if (tabId === "dashboard") { - renderGoals(); - } else if (tabId === "simulator") { - // Re-render simulator to draw SVG chart with correct width - updateSimulator(); - } else if (tabId === "passport") { - renderPassportTab(); - } else if (tabId === "learning") { - renderLearningTab(); + if (tabHandlers[tabId]) { + tabHandlers[tabId](); } // Re-apply jargon translation state to newly rendered elements @@ -73,14 +74,8 @@ document.addEventListener('DOMContentLoaded', () => { jargonSimplified = simplified; // Refresh the active tab elements to translate them - if (currentTab === "dashboard") { - renderGoals(); - } else if (currentTab === "simulator") { - updateSimulator(); - } else if (currentTab === "passport") { - renderPassportTab(); - } else if (currentTab === "learning") { - renderLearningTab(); + if (tabHandlers[currentTab]) { + tabHandlers[currentTab](); } }); From 871c2d08d452546d03af52bbdfe2ad0b973d0f6e Mon Sep 17 00:00:00 2001 From: "Kumar,Avinash,IN-Bangalore" Date: Fri, 18 Sep 2026 18:50:20 +0530 Subject: [PATCH 2/2] feat: add investment simulator module with real mutual fund NAV data integration, goals, jargon, and security configurations --- .github/dependabot.yml | 9 +- index.html | 1 + js/goals.js | 22 +- js/jargon.js | 27 ++- js/simulator.js | 59 ++++-- security/scan.md | 456 +++++++++++++++++++++++++++++++++++++++++ security/scan2.md | 170 +++++++++++++++ skills-lock.json | 11 + 8 files changed, 720 insertions(+), 35 deletions(-) create mode 100644 security/scan.md create mode 100644 security/scan2.md create mode 100644 skills-lock.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 91560a1..2a911dd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,10 +5,11 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" + # Note: The 'npm' ecosystem is disabled until package.json is initialized in the root. + # - package-ecosystem: "npm" + # directory: "/" + # schedule: + # interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/index.html b/index.html index 02d0b13..98099e9 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ TrueNorth (India Edition) - Goal-First Investing + diff --git a/js/goals.js b/js/goals.js index 045c5fb..5156639 100644 --- a/js/goals.js +++ b/js/goals.js @@ -2,7 +2,7 @@ TrueNorth Goals & Portfolio Mapping Module ========================================================================== */ -import { t } from './jargon.js'; +import { t, escapeHTML } from './jargon.js'; // Pre-populated active goals let goals = [ @@ -77,12 +77,16 @@ function getPortfolioConfig(years) { * Calculates the monthly SIP required using compound interest * SIP = Target * r / ((1 + r)^n - 1) */ -function calculateRequiredSIP(target, years, rate) { +export function calculateRequiredSIP(target, years, rate) { + if (!target || target <= 0 || !years || years <= 0 || !rate || rate <= 0) { + return 0; + } const r = rate / 12; const n = years * 12; - if (r === 0) return Math.round(target / n); - const sip = (target * r) / (Math.pow(1 + r, n) - 1); - return Math.round(sip); + const denom = Math.pow(1 + r, n) - 1; + if (denom <= 0) return Math.round(target / n); + const sip = (target * r) / denom; + return Number.isFinite(sip) ? Math.round(sip) : 0; } /** @@ -140,7 +144,7 @@ export function renderGoals() {
-

${goal.title}

+

${escapeHTML(goal.title)}

Target: ${formatINR(goal.targetAmount)}
@@ -272,7 +276,7 @@ function renderWizard() {
- +
@@ -328,7 +332,7 @@ function renderWizard() {
Target Goal - ${goalName || 'My Goal'} (${formatINR(targetAmount)}) + ${escapeHTML(goalName || 'My Goal')} (${formatINR(targetAmount)})
Required Monthly Savings (${t('SIP')}) @@ -443,7 +447,7 @@ function createNewGoal() { const newGoal = { id: Date.now(), - title: goalName || "My Savings Goal", + title: (goalName && goalName.trim()) ? goalName.trim() : "My Savings Goal", category: selectedCategory || "custom", icon: categoryIcons[selectedCategory] || "🎯", targetAmount: targetAmount, diff --git a/js/jargon.js b/js/jargon.js index 0f07e64..24613ec 100644 --- a/js/jargon.js +++ b/js/jargon.js @@ -69,6 +69,21 @@ export const jargonDictionary = { // Global state for translation let isSimplified = false; +/** + * Safely escapes HTML special characters to prevent DOM-based XSS + * @param {*} str - Raw string or value to escape + * @returns {string} Escaped HTML-safe string + */ +export function escapeHTML(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + /** * Helper to generate HTML for a jargon term * @param {string} key - Dictionary key @@ -77,18 +92,18 @@ let isSimplified = false; */ export function t(key, overrideText) { const item = jargonDictionary[key]; - if (!item) return overrideText || key; + if (!item) return escapeHTML(overrideText || key); - const text = isSimplified ? item.simplified : (overrideText || item.jargon); - const tooltipText = isSimplified + const rawText = isSimplified ? item.simplified : (overrideText || item.jargon); + const rawTooltip = isSimplified ? `Original term: "${item.jargon}". ${item.explanation}` : item.explanation; const className = isSimplified ? "jargon-term jargon-translated" : "jargon-term"; - return ` - ${text} - ${tooltipText} + return ` + ${escapeHTML(rawText)} + ${escapeHTML(rawTooltip)} `; } diff --git a/js/simulator.js b/js/simulator.js index a1058ab..51e7a06 100644 --- a/js/simulator.js +++ b/js/simulator.js @@ -3,7 +3,7 @@ Enhanced to use real mutual fund NAV data from MFapi.in service ========================================================================== */ -import { t } from './jargon.js'; +import { t, escapeHTML } from './jargon.js'; import { formatINR } from './goals.js'; import { MFapiService } from './services/mfapi.js'; @@ -43,13 +43,33 @@ async function calculateDataSeries() { return series; } +/** + * Safely parses Indian DD-MM-YYYY date format into a Date object + * @param {string} dateStr - Date string in DD-MM-YYYY or ISO format + * @returns {Date} Parsed Date object + */ +function parseIndianDate(dateStr) { + if (!dateStr || typeof dateStr !== 'string') return new Date(0); + const parts = dateStr.trim().split('-'); + if (parts.length === 3) { + const day = Number(parts[0]); + const month = Number(parts[1]); + const year = Number(parts[2]); + if (!isNaN(day) && !isNaN(month) && !isNaN(year) && year > 1900) { + return new Date(year, month - 1, day); + } + } + const fallback = new Date(dateStr); + return isNaN(fallback.getTime()) ? new Date(0) : fallback; +} + /** * Calculate series using projected returns (original logic) */ function calculateProjectedSeries(months) { const series = []; - const rateTN = RATES[riskProfile]; - const rateFD = RATES.FD; + const rateTN = RATES[riskProfile] || 0.105; + const rateFD = RATES.FD || 0.065; const monthlyRateTN = rateTN / 12; const monthlyRateFD = rateFD / 12; @@ -61,16 +81,20 @@ function calculateProjectedSeries(months) { let tnValue = 0; if (m > 0) { - fdValue = sipAmount * ((Math.pow(1 + monthlyRateFD, m) - 1) / monthlyRateFD) * (1 + monthlyRateFD); - tnValue = sipAmount * ((Math.pow(1 + monthlyRateTN, m) - 1) / monthlyRateTN) * (1 + monthlyRateTN); + fdValue = monthlyRateFD > 0 + ? sipAmount * ((Math.pow(1 + monthlyRateFD, m) - 1) / monthlyRateFD) * (1 + monthlyRateFD) + : invested; + tnValue = monthlyRateTN > 0 + ? sipAmount * ((Math.pow(1 + monthlyRateTN, m) - 1) / monthlyRateTN) * (1 + monthlyRateTN) + : invested; } series.push({ month: m, year: (m / 12).toFixed(1), - invested: Math.round(invested), - fd: Math.round(fdValue), - truenorth: Math.round(tnValue) + invested: Math.round(Number.isFinite(invested) ? invested : 0), + fd: Math.round(Number.isFinite(fdValue) ? fdValue : 0), + truenorth: Math.round(Number.isFinite(tnValue) ? tnValue : 0) }); } @@ -88,11 +112,11 @@ async function calculateHistoricalSeries(navData, months) { return calculateProjectedSeries(months); } - // Sort NAV data by date (oldest first) + // Sort NAV data by date (oldest first) using robust date parsing const sortedData = [...navData].sort((a, b) => { - const [dayA, monthA, yearA] = a.date.split('-').reverse().join('-').split('-').map(Number); - const [dayB, monthB, yearB] = b.date.split('-').reverse().join('-').split('-').map(Number); - return new Date(yearA, monthA-1, dayA) - new Date(yearB, monthB-1, dayB); + const timeA = parseIndianDate(a.date).getTime(); + const timeB = parseIndianDate(b.date).getTime(); + return timeA - timeB; }); // We want to simulate investing over the last 'historicalPeriod' years @@ -414,9 +438,10 @@ export async function updateSimulator() { const noteEl = document.getElementById('sim-note-text'); if (noteEl) { if (isHistoricalMode) { - const schemeName = await getSchemeName(selectedSchemeCode) || selectedSchemeCode; + const rawSchemeName = await getSchemeName(selectedSchemeCode) || selectedSchemeCode; + const safeScheme = t(escapeHTML(rawSchemeName)); noteEl.innerHTML = ` - This simulation reflects actual historical NAV data of the ${t(schemeName)} + This simulation reflects actual historical NAV data of the ${safeScheme} mutual fund over the last ${historicalPeriod} years. In comparison, a traditional Fixed Deposit grew at 6.5%. `; @@ -427,11 +452,13 @@ export async function updateSimulator() { AGGRESSIVE: "growth-oriented stock funds (top 50 Indian companies)" }; const ratePct = (RATES[riskProfile] * 100).toFixed(1); + const safeRisk = escapeHTML(riskProfile.toLowerCase()); + const safeAllocation = escapeHTML(allocationMap[riskProfile] || "balanced allocation"); noteEl.innerHTML = ` This projection assumes an average annual return of ${ratePct}%, - representing a ${riskProfile.toLowerCase()} strategy allocated in - ${allocationMap[riskProfile]}. + representing a ${safeRisk} strategy allocated in + ${safeAllocation}. Fixed Deposits are calculated at a steady 6.5% annual return. `; } diff --git a/security/scan.md b/security/scan.md new file mode 100644 index 0000000..336f4c5 --- /dev/null +++ b/security/scan.md @@ -0,0 +1,456 @@ +# TrueNorth Security Audit & Vulnerability Assessment Report + +**Target**: TrueNorth (India Edition) +**Date**: September 18, 2026 +**Auditor**: Antigravity Security Audit Engine +**Assessment Type**: Defensive Source-First Codebase Audit +**Artifact Path**: `security/scan.md` + +--- + +## 1. Executive Summary + +A comprehensive source-level security audit of the **TrueNorth (India Edition)** codebase was conducted. TrueNorth is an early-stage, goal-first financial education and simulation platform built with modern modular JavaScript (ES6), HTML5, and CSS3. + +The audit evaluated application architecture, trust boundaries, user input handling, rendering logic, external service simulation, browser-side state management, and configuration hygiene. + +### Overall Security Posture: Moderate Risk (Pre-Production Sandbox) +While the application currently operates entirely in the browser client without server-side persistence or active payment gateways, multiple vulnerabilities—most notably **DOM-based Cross-Site Scripting (XSS)** and the **absence of Content Security Policy (CSP)**—exist. Left unmitigated, these vulnerabilities will pose critical risks as TrueNorth transitions into Phase 3 (Payments, KYC, PAN/Aadhaar handling, and mutual fund execution). + +### Vulnerability Summary + +| Finding ID | Title | Severity | OWASP Top 10 | Status | +| :--- | :--- | :--- | :--- | :--- | +| **TN-SEC-01** | DOM-based Cross-Site Scripting (XSS) via Unsanitized Goal Title | **High** | A03:2021-Injection | **Confirmed** | +| **TN-SEC-02** | Absence of Content Security Policy (CSP) & Frame Controls | **Medium** | A05:2021-Security Misconfiguration | **Confirmed** | +| **TN-SEC-03** | Insecure HTML String Interpolation in Jargon Translation Engine | **Medium** | A03:2021-Injection | **Confirmed** | +| **TN-SEC-04** | Client-Side Mathematical Boundary Violations (Division by Zero / NaN) | **Low** | A04:2021-Insecure Design | **Confirmed** | +| **TN-SEC-05** | Fragile Date Parsing and Error Handling in NAV Series Generation | **Low** | A04:2021-Insecure Design | **Confirmed** | +| **TN-SEC-06** | Missing Subresource Integrity (SRI) for Third-Party Assets | **Informational** | A08:2021-Software & Data Integrity Failures | **Confirmed** | +| **TN-SEC-07** | Dependabot Manifest Misconfiguration (Missing `package.json`) | **Informational** | A05:2021-Security Misconfiguration | **Confirmed** | + +--- + +## 2. Architecture & Threat Model + +### 2.1 Component Breakdown & Data Flow + +```mermaid +flowchart TD + User([User Browser]) + + subgraph ClientApp["TrueNorth Client (Browser DOM)"] + IndexHTML["index.html"] + App["app.js (Router / Coordinator)"] + Goals["goals.js (Goal Wizard & State)"] + Sim["simulator.js (SVG Chart & Math)"] + Passport["passport.js (Global Assets)"] + Learning["learning.js (Interactive Hub)"] + Jargon["jargon.js (DOM Translator)"] + MFapi["services/mfapi.js (Mock Service)"] + end + + subgraph ExternalServices["External Endpoints (Present & Planned)"] + GFONTS["Google Fonts (fonts.googleapis.com)"] + MOCK_API["MFapi.in (Planned Live Endpoint)"] + PAYMENTS["Razorpay / UPI (Planned Phase 3)"] + KYC["Digilocker / CKYC (Planned Phase 3)"] + end + + User -->|Input: Goal Name, Sliders, Toggles| IndexHTML + IndexHTML --> App + App --> Goals + App --> Sim + App --> Passport + App --> Learning + Goals -->|t() markup| Jargon + Sim -->|t() markup| Jargon + Sim --> MFapi + Passport -->|t() markup| Jargon + IndexHTML -.->|Load Stylesheet & Fonts| GFONTS + MFapi -.->|Future HTTP requests| MOCK_API +``` + +### 2.2 Trust Boundaries + +1. **User Input vs. DOM Sink Boundary**: User input from input fields (``) crosses directly into `Element.innerHTML` sinks without encoding or sanitization. +2. **Third-Party API vs. Client Display Boundary**: Data returned from external API endpoints (`MFapi.in` or similar) is treated as trusted strings when generating UI explanations. +3. **Client vs. Future Execution Boundary**: In Phase 3, client-calculated SIP values, user identities, and investment amounts must never be trusted by downstream backend/payment services. + +--- + +## 3. Confirmed Findings & Detailed Analysis + +--- + +### [TN-SEC-01] DOM-based Cross-Site Scripting (XSS) via Unsanitized Goal Title + +- **Severity**: **High** +- **CVSS v3.1**: 7.4 (`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N`) +- **Affected Files**: + - [goals.js](file:///c:/Users/avinash.cm.kumar/personal/TrueNorth/js/goals.js#L131-L145) (Lines 131–145, 275, 331) +- **CWE**: CWE-79 (Improper Neutralization of Input During Web Page Generation) + +#### Vulnerability Mechanism +In [goals.js](file:///c:/Users/avinash.cm.kumar/personal/TrueNorth/js/goals.js), user input from the goal creation wizard is bound to the `goalName` state variable without sanitization: + +```javascript +// js/goals.js: line 375-379 +const nameInput = document.getElementById('goal-name-input'); +if (nameInput) { + nameInput.addEventListener('input', (e) => { + goalName = e.target.value; + }); +} +``` + +This variable is directly concatenated into HTML strings assigned to `Element.innerHTML` in two places: + +1. **In the Wizard Preview (Step 3)**: + ```javascript + // js/goals.js: line 331 +
+ Target Goal + ${goalName || 'My Goal'} (${formatINR(targetAmount)}) +
+ ``` +2. **In the Active Goals Dashboard (`renderGoals`)**: + ```javascript + // js/goals.js: line 143 +
+

${goal.title}

+
Target: ${formatINR(goal.targetAmount)}
+
+ ``` +3. **In the Input Value Attribute (Step 2)**: + ```javascript + // js/goals.js: line 275 + + ``` + If `goalName` contains quotes (e.g. `" onfocus="alert(1)" autofocus="`), it breaks out of the `value` attribute into an inline event handler. + +#### Concrete Attack Scenario +1. An attacker constructs a payload or tricks a user into entering a crafted goal title (or imports goals via future URL params / shared state): + ```html + + ``` +2. The user advances from Step 2 to Step 3 in the goal wizard. +3. `renderWizard()` sets `container.innerHTML = ...`, immediately executing the JavaScript payload within the application's origin context. +4. When the goal is saved and rendered on the dashboard, `renderGoals()` executes the payload again every time the user visits the dashboard. + +#### Impact +Execution of arbitrary JavaScript in the victim's session. In future iterations where authentication cookies, JWT tokens, CKYC records, or payment sessions exist, an attacker could extract credentials, tamper with simulated numbers, or redirect users to phishing flows. + +#### Remediation +Implement an HTML entity encoder and use safe text node assignments or sanitize all dynamic variables prior to `innerHTML` interpolation. + +```javascript +// Utility function: escapeHTML +export function escapeHTML(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} +``` + +Apply `escapeHTML(goal.title)` and `escapeHTML(goalName)` in [goals.js](file:///c:/Users/avinash.cm.kumar/personal/TrueNorth/js/goals.js). + +--- + +### [TN-SEC-02] Absence of Content Security Policy (CSP) & Frame Controls + +- **Severity**: **Medium** +- **CVSS v3.1**: 5.4 (`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N`) +- **Affected Files**: + - [index.html](file:///c:/Users/avinash.cm.kumar/personal/TrueNorth/index.html#L1-L14) +- **CWE**: CWE-1021 (Improper Restriction of Rendered UI Layers / Clickjacking), CWE-358 + +#### Vulnerability Mechanism +[index.html](file:///c:/Users/avinash.cm.kumar/personal/TrueNorth/index.html) does not define any `Content-Security-Policy` meta tag or HTTP response headers: +- There is no restriction on which domains can frame the application (`frame-ancestors`), leaving it vulnerable to clickjacking or UI redressing attacks. +- There is no restriction on script execution sources (`script-src`), allowing inline injection scripts (such as TN-SEC-01) to execute freely. +- There are no restrictions on outbound network connections (`connect-src`), allowing injected scripts to exfiltrate DOM data to arbitrary attacker-controlled servers. + +#### Concrete Attack Scenario +1. An attacker frames TrueNorth inside a transparent `