# SCAN Briefing: humand-web — 2026-02-13 ## Target Profile - **Repo**: https://github.com/HumandDev/humand-web (private) - **Clone**: ~/Code/humand/humand-web - **Platform**: internal review (not bug bounty) - **Stack**: React 18 + TypeScript + Vite + MUI + Axios + Socket.IO - **Auth**: JWT (access + refresh tokens), SSO (Azure AD, Okta, Google), OTP, SAML/IDP - **Files**: 2,564 .ts/.tsx source files - **Features**: Feed, chat, groups, forms, documents, SCORM courses, marketplace, performance reviews, time tracking, video calls, employee lifecycle, service management - **Mobile bridge**: React Native WebView via postMessage + URL token passing ## Scan Method Manual code review focused on: auth flows, token handling, HTML rendering, secrets, access control, third-party integrations. --- ## Findings Summary | # | ID | Severity | Confidence | Title | Status | |---|-----|----------|------------|-------|--------| | 1 | 20 | **HIGH** | high | Hardcoded SCORM Cloud API credentials | triaged | | 2 | 21 | **HIGH** | high | Refresh tokens in URL path params | triaged | | 3 | 22 | MEDIUM | high | Access tokens in URL path params | triaged | | 4 | 23 | MEDIUM | high | postMessage handler—no origin check | triaged | | 5 | 24 | **HIGH** | medium | Stored XSS via dangerouslySetInnerHTML (8 sinks) | triaged | | 6 | 25 | MEDIUM | high | EncryptStorage key in bundle | triaged | | 7 | 26 | MEDIUM | medium | document.domain relaxation | triaged | --- ## Finding Details ### F1 — Hardcoded SCORM Cloud API Credentials (ID 20) **Severity**: HIGH | **Confidence**: HIGH | **CWE-798** **Location**: `src/config/api.ts:98-104` ```typescript export const scormApi = axios.create({ baseURL: 'https://cloud.scorm.com/api/v2', headers: { Authorization: 'Basic NjU3TjRJUllDVDpnVmFSWTJEUExDZ215SUIzeVA2a2tDUmpZcjE4aFJzOUVxMkx2cVdS', }, }); ``` Decoded: `657N4IRYCT:gVaRY2DPLCgmyIB3yP6kkCRjYr18hRs9Eq2LvqWR` **Impact**: Anyone who inspects the frontend bundle can extract these credentials and call the SCORM Cloud API v2 directly — reading courses, registrations, learner data; potentially modifying training content. **Fix**: Move to backend. Frontend should call your own API which proxies to SCORM Cloud with server-side credentials. --- ### F2 — Refresh Tokens Exposed in URL Path (ID 21) **Severity**: HIGH | **Confidence**: HIGH | **CWE-598** **Locations**: - `src/routes.tsx:2505` — `/documents-lacomer-mobile/:refreshToken` - `src/routes.tsx:2514` — `/requests-banbajio-mobile/:refreshToken` **Impact**: Refresh tokens are long-lived. They leak via: - Browser history - Referer headers to third-party resources - Server/proxy access logs - Analytics (Amplitude, Sentry, Clarity are all configured) - Shoulder surfing A leaked refresh token = full account takeover until revoked. **Fix**: Use postMessage or a short-lived one-time code exchanged for tokens via a backend endpoint. Never pass tokens in URLs. --- ### F3 — Access Tokens Exposed in URL Path (ID 22) **Severity**: MEDIUM | **Confidence**: HIGH | **CWE-598** **Locations**: - `src/routes.tsx:1298` — `/events-nemak-mobile/:accessToken` - `src/routes.tsx:2476` — `/org-chart-mobile/:accessToken/:id` - `src/routes.tsx:2485` — `/scorm-courses-mobile/:accessToken` - `src/routes.tsx:2494` — `/recognitions-nemak-mobile/:accessToken` Additionally, `src/pages/dashboard/scorm/Folders.tsx:64-65` stores the URL token directly to encryptStorage as the active session: ```typescript if (accessToken && isMobile) { encryptStorage.setItem('accessToken', accessToken); } ``` Same leak vectors as F2 but with shorter-lived tokens. --- ### F4 — postMessage Handler Without Origin Validation (ID 23) **Severity**: MEDIUM | **Confidence**: HIGH | **CWE-346** **Location**: `src/hooks/useMobileToken.ts:6-12` ```typescript const handleMessage = event => { if (event.data.type === 'humand') { encryptStorage.setItem('accessToken', event.data.token); } }; window.addEventListener('message', handleMessage); ``` No `event.origin` check. Any page that can get a reference to this window (iframe, window.open) can inject/overwrite the stored access token. **Impact**: Session fixation. Attacker sends `{type: "humand", token: "attacker-jwt"}` → victim's session is hijacked. **Fix**: Add strict origin validation: `if (event.origin !== expectedOrigin) return;` --- ### F5 — Stored XSS via dangerouslySetInnerHTML (ID 24) **Severity**: HIGH | **Confidence**: MEDIUM | **CWE-79** 8 components render HTML from server/user data via `dangerouslySetInnerHTML` with **no sanitization** (no DOMPurify, no sanitize-html): | File | Line | Source of HTML | |------|------|----------------| | `src/components/HTMLBody.tsx` | 67 | `body` prop (posts, news articles) | | `src/pages/dashboard/tickets/components/FAQDialog.tsx` | 58 | `body` prop (FAQ content) | | `src/components/text/ShowMoreText.tsx` | 93 | `text` prop when `isHtmlText=true` | | `src/pages/dashboard/performance/components/RichText.tsx` | 38-39 | `text` prop (performance review text) | | `src/pages/dashboard/libraries/components/LibrarySearchItem.tsx` | 78-80, 122-125 | `library.highlights.title`, `library.highlights.textContent` | | `src/pages/dashboard/banBajio/useLibraryInfoDrawer.tsx` | 22 | `library.body` | | `src/pages/dashboard/banBajio/LibraryFullScreen.tsx` | 41 | `library.body` | | `src/components/Highlighter.tsx` | 83-84 | `transformTagInStrongElement(body, taggedUsers)` | The DOMParser usage in HTMLBody.tsx and FAQDialog.tsx is **not sanitization** — it only modifies anchor attributes but passes through `