Important
This project is currently not release-ready.
This is a project of the Apache Software Foundation and follows the ASF vulnerability handling process.
Check the Projects page for project-specific security information.
To report a new vulnerability you have discovered please follow the ASF vulnerability reporting process.
This document is for human security researchers finding and submitting security reports, AI-assisted human security researchers doing the same, and project maintainers handling these reports.
This is not a standalone document; it lives alongside the code it supports, and it should be read together with the Apache Fineract Threat Model which governs the API layer that this UI consumes.
See the project README for an introduction to the Fineract Backoffice UI.
Source code repository: https://github.com/apache/fineract-backoffice-ui
This document covers the Angular 21 single-page application (SPA) only — the client-side code that runs in a browser and communicates with the Apache Fineract REST API. Backend security (database, JDBC, Spring Security, Kafka, COB batch) is out of scope here and is addressed by the Fineract server-side threat model.
- Back-office core banking UI: Loan officers, tellers, and administrators manage clients,
loans, savings, deposits, accounting, and organizational configuration through a browser-based
interface that calls Fineract's REST API (
/fineract-provider/api/v1). - Role-adaptive interface: The UI hides or exposes menu items and action buttons based on
the granular Fineract permissions returned at login, via the
*appHasPermissionstructural directive. - Multi-tenant operation: A single deployed instance serves multiple Fineract tenants,
differentiated by a
Fineract-Platform-TenantIdheader injected byauthInterceptoron every outgoing request.
| Context | Description |
|---|---|
| Docker / NGINX | Production path: npm run build output served from NGINX (deploy/nginx.conf); FINERACT_API_URL and DEFAULT_TENANT injected via deploy/entrypoint.sh into /config.json at container start. |
| Standalone NGINX | Static files (dist/) served by operator-managed NGINX; operator sets config.json manually. |
| Angular dev server | npm start — HTTPS on port 4200 via ssl/localhost.{key,crt}; proxies /fineract-provider to a local Fineract instance. Development-only; must not be used in production. |
| Integrated reverse-proxy | UI and API share a single domain via a reverse proxy (/ → SPA; /fineract-provider → Fineract), eliminating cross-origin requests at runtime. |
| Role | Trust level | Description |
|---|---|---|
| Operator / deployer | Full container / web-server access | Configures FINERACT_API_URL, TLS termination, NGINX headers, CSP. |
| Authenticated back-office user (low-privilege) | Valid Fineract session, limited permissions | Loan officers, tellers — scope-limited by Fineract RBAC. |
| Authenticated back-office user (high-privilege) | Valid Fineract session, admin role | Branch managers, system admins — can perform most mutations. |
| Unauthenticated browser user | Zero trust | Can only reach the /login page; all other routes redirect via authGuard. |
- Apache Fineract backend (Spring Boot, database, Kafka, COB jobs) — covered by the Fineract server-side threat model.
- Self-service or customer-facing portals — the UI is back-office only; bank customers are never direct users.
- Mobile / native applications — this model covers web browsers only.
- Third-party analytics or monitoring scripts — no such scripts are bundled; operators who add them bear responsibility for the resulting threat surface expansion.
- OpenAPI client (
src/app/api/) — generated code, never hand-edited; security properties of the generated HTTP calls are covered in §8.
- Physical access to the browser host: If an attacker has OS-level access to the end user's machine (e.g., keylogger, RAM dump), the model assumes the game is already lost.
- Network-level DDoS against the SPA's web server: NGINX connection limits are a starting point; volumetric DDoS mitigation is explicitly left to the CDN / reverse proxy / cloud provider.
- Browser zero-days: Exploitation of unpatched browser vulnerabilities is out of model.
- Supply-chain attack on
node_modules: The model assumes npm dependencies are not themselves compromised; operator dependency scanning (e.g., Dependabot,npm audit) is a downstream responsibility.
The browser sandbox is the primary trust boundary for this application. JavaScript
executing in the browser is untrusted code from the perspective of the Fineract API, even
when a valid Authorization header is attached (the API must enforce its own RBAC).
The /config.json file is a secondary trust boundary: whoever controls the web server
controls the fineractApiUrl the SPA will use. A compromised config.json can redirect all
API calls (including credentials) to an attacker-controlled host.
The sessionStorage boundary: the session object (including the base64EncodedAuthenticationKey)
is stored in sessionStorage, scoped to the browser tab. It is readable by any JavaScript
executing on the same origin.
The localStorage boundary: fineract_tenant (tenant ID) and fineract_runtime_config
(API URL override) are stored in localStorage, shared across all tabs of the same origin.
These values are operator-intent configuration and treated as trusted inputs.
[Browser User]
|
| (HTTPS, served by NGINX or CDN)
v
[Angular SPA — browser sandbox]
|
| (HTTPS, same-origin or CORS-allowed cross-origin)
| Authorization: Basic <base64-key>
| Fineract-Platform-TenantId: <tenant>
| X-Correlation-ID: <uuid>
v
[Fineract REST API — /fineract-provider/api/v1]
|
| (JDBC, server-side)
v
[Database (PostgreSQL / MariaDB)]
- Browser → NGINX (SPA assets): TLS-encrypted. The operator must supply a CA-trusted certificate or terminate TLS upstream. The shipped NGINX config listens on port 80 only (HTTP); TLS must be layered above it in production.
- Browser → Fineract API: TLS-encrypted. The Angular
authInterceptorattachesAuthorization: Basic <token>and tenant headers to every API request. The token is thebase64EncodedAuthenticationKeyreturned by the Fineract/authenticationendpoint and stored insessionStorage. - Container start →
config.json: Theentrypoint.shwritesFINERACT_API_URLandDEFAULT_TENANTenvironment variables into/usr/share/nginx/html/config.json. If environment variables are unset, the shipped default (/fineract-provider/api/v1) is used. The SPA reads this file at bootstrap viaConfigService.loadConfig().
- A modern evergreen browser (Chrome, Firefox, Edge, Safari) that enforces the Same-Origin Policy (SOP), Content Security Policy (CSP), and HttpOnly / SameSite cookie semantics.
- JavaScript is enabled. The SPA does not support a JS-disabled mode.
- NGINX (or equivalent) correctly sets TLS and serves the SPA assets with appropriate cache
headers. Static assets are cached for 6 months (
Cache-Control: public) pernginx.conf;index.htmlmust not be cached for longer than the deployment cycle.
- Angular's built-in XSS protections (automatic HTML escaping via the template engine,
DomSanitizerfor dynamic content) are not bypassed by application code. Use ofbypassSecurityTrust*APIs is treated as aVALID-HARDENINGtrigger.
- All browser-to-API communication is over TLS in production.
- The Angular dev server's
proxy.conf.jsonsets"secure": false(skips TLS verification for the upstream Fineract connection). This is a dev-only convenience and must not be replicated in production infrastructure.
- The
currentTenantIdsignal inAuthServiceis initialised fromlocalStorageat startup. In a multi-tab scenario, a tenant change in one tab does not automatically propagate to others until the signal is updated (e.g., on next user action).
The SPA supports the following runtime-configurable knobs:
| Knob | Source | Default | Security relevance |
|---|---|---|---|
FINERACT_API_URL |
Container env var → config.json |
/fineract-provider/api/v1 (same-origin proxy) |
Controls where API calls are sent; must point to a TLS-protected Fineract instance. |
DEFAULT_TENANT |
Container env var → config.json |
default |
Pre-populates the tenant field on the login form. |
fineract_runtime_config |
localStorage user override |
Falls back to config.json |
A user (or XSS payload) can override the API URL for their browser session via ConfigService.setApiUrl(). |
| Angular environment | --configuration build flag |
environment.ts (dev) |
The production configuration enables Angular's production mode (disables debug tooling). Must be used for all production builds. |
rbacEnabled |
environment.ts / environment.prod.ts build flag |
true |
Enables UI-side role-based access control. When false, the sidebar shows all navigation items and the *appHasPermission / *appInstitutionFeature directives render everything. UI convenience only — not a security boundary (see §11.7). Provided so pre-RBAC deployments can upgrade without an immediate visibility change, enabling RBAC per-environment when ready. |
The rbacEnabled flag gates two structural directives and the sidebar's navigation filtering:
*appHasPermission(src/app/shared/directives/has-permission.directive.ts) — checksAuthService.hasPermission(). WhenrbacEnabled === false, it renders unconditionally.*appInstitutionFeature(src/app/shared/directives/has-institution-feature.directive.ts) — checksInstitutionConfigService.isFeatureEnabled()for group-lending features (groups,centers,collection_sheet). WhenrbacEnabled === false, it renders unconditionally.
Because these are UI-visibility controls only, toggling rbacEnabled never widens or narrows
what the Fineract API will authorize. Authorization is always enforced server-side. Disabling the
flag therefore restores the legacy "everything visible" experience without weakening any real
security property, and enabling it does not substitute for server-side permission checks.
| Source | Trust | Notes |
|---|---|---|
| User-typed form fields (login, client data, loan amounts) | Untrusted | Angular template engine escapes all interpolated values; must not be bypassed. |
| Fineract API responses (JSON) | Partially trusted | Trusted for data, not for executable content. Angular's HTTP client does not auto-execute response bodies. |
config.json (served as a static file) |
Operator-trusted | Whoever controls the web server controls this file. If an attacker can modify it, they can redirect credentials. |
localStorage / sessionStorage |
User-trusted, not operator-trusted | Readable and writable by any script on the same origin; treated as ephemeral, not as a secure store. |
| URL path / query parameters | Untrusted | Parsed by Angular Router; no eval or innerHTML injection from route params in current code. |
X-Forwarded-For, X-Forwarded-Proto |
Not read by the SPA | Only relevant to the Fineract server; out of scope here. |
- API response size: Not explicitly bounded by the SPA; very large responses (e.g.,
un-paginated list endpoints) could cause UI lag. Per
ARCHITECTURE_DECISIONS.md, server-side pagination is the mandated pattern for all data grids. - Rate limiting: Not enforced by the SPA. The SPA does not implement any client-side throttling of its API calls. Left to the reverse proxy / Fineract server.
| Adversary | Capability | What they are trying to do |
|---|---|---|
| Passive network observer | Can observe TLS-encrypted traffic but cannot decrypt it without certificate compromise. | Infer session existence; cannot read credentials or data in transit if TLS is correctly configured. |
| Active network attacker (MitM) | Can intercept HTTP traffic if TLS is misconfigured or absent. | Steal Authorization headers and session data in transit; serve modified SPA assets. |
| Unauthenticated browser user | Can load the SPA and reach the /login page; all other routes are blocked by authGuard. |
Brute-force login (credential stuffing); exploit the login form for XSS via error message rendering. |
| Authenticated back-office user (low-privilege) | Has a valid Fineract session with limited permissions; *appHasPermission hides high-privilege UI elements. |
Discover hidden routes via URL manipulation; call API endpoints not exposed in the UI; exfiltrate data visible in their permitted views; hijack another user's session via XSS. |
| Authenticated back-office user (high-privilege) | Has a valid Fineract admin session; can reach all UI routes and trigger all API calls the UI exposes. | Use the UI as a vector to exfiltrate data; inject content into fields rendered to other users (stored XSS). |
| Malicious third-party script | Can execute JavaScript on the same origin if CSP is absent or misconfigured. | Read sessionStorage (steal auth token); read localStorage (steal/modify API URL); exfiltrate data from the DOM. |
- Physical attacker with access to the user's device: If the attacker has OS-level access, the model assumes they have already won.
- Browser zero-day exploiter: Exploitation of unpatched browser engine vulnerabilities is not modelled.
- Supply-chain attacker (npm): Compromise of a dependency via a malicious npm package is out of scope for this model; it is addressed by dependency-scanning tooling.
- Fineract API attacker: Threats to the backend REST API are covered by the Fineract server-side threat model; the SPA cannot defend against them independently.
- Route-level authentication enforcement —
authGuardredirects unauthenticated users to/loginfor all protected routes. The SPA never renders back-office content before a valid session is established. - Automatic session injection —
authInterceptorattachesAuthorization: Basic <token>and tenant headers to every outgoing API request, preventing accidental unauthenticated calls. - UI-level RBAC —
*appHasPermissionstructural directive dynamically shows or hides UI elements based on the granular permission list returned by Fineract at login. TheALL_FUNCTIONS/ALL_FUNCTIONS_READsuper-permissions are correctly short-circuited. - Session scoped to browser tab — the authentication token (
base64EncodedAuthenticationKey) is stored insessionStorage(tab-scoped), notlocalStorage(origin-scoped). Closing the tab clears the session. - Automatic idle logout —
IdleServiceforces logout after 15 minutes of inactivity, with a 2-minute warning dialog. This limits the window of exposure for unattended sessions. - Correlation ID tracing —
correlationIdInterceptoradds aX-Correlation-IDUUID header to every request, enabling server-side audit log correlation. - XSS protection via Angular templates — Angular's template engine auto-escapes all
interpolated values. No
innerHTML,bypassSecurityTrustHtml, orevalusages exist in current application code. - Runtime API URL configuration —
ConfigServicefetchesconfig.jsonat bootstrap and caches the result, preventing hard-coded API URL issues across environments. - Error message sanitisation —
errorInterceptorextracts structured error fields (defaultUserMessage,developerMessage,parameterName) from API responses rather than reflecting raw response bodies into the DOM.
- Content Security Policy (CSP) — no
Content-Security-Policyheader is set by the shippednginx.conf. The operator must add it. Without CSP, injected scripts can readsessionStorageand exfiltrate auth tokens. - HTTP security headers —
nginx.confdoes not includeX-Frame-Options,X-Content-Type-Options,Strict-Transport-Security, orReferrer-Policy. These must be added by the operator. - TLS on the NGINX listener — the shipped
nginx.conflistens on port 80 (HTTP only). TLS must be configured by the operator or terminated upstream. Without it, credentials and session tokens are transmitted in plaintext. - Client-side rate limiting / lockout — no attempt-counting or CAPTCHA is implemented on the login form. Brute-force protection is left to the Fineract API and the reverse proxy.
- Route-level RBAC (beyond
authGuard) — theauthGuardonly checksisAuthenticated(). A low-privilege user who knows a URL can navigate to a high-privilege route; the page will load, and then fail at the API layer (Fineract enforces RBAC server-side). No client-side permission check gates route activation beyond login status. sessionStorage/localStorageencryption — auth tokens and config are stored in plaintext browser storage. Any script on the same origin can read them. (ARCHITECTURE_DECISIONS.mdnotes encrypted storage as a future goal via Web Crypto API; it is not implemented yet.)- Audit log for UI actions — a telemetry service is scaffolded but not implemented. There is no persistent, tamper-resistant record of which back-office users performed which actions via the UI.
- Subresource Integrity (SRI) — Google Fonts and Material Icons are loaded from external
CDNs in
index.htmlwithoutintegrityattributes, making them a potential supply-chain injection vector if the CDN is compromised. - Token refresh / rotation — the
base64EncodedAuthenticationKeyissued by Fineract is used for the lifetime of the session without rotation. The SPA does not implement token refresh. - Multi-factor authentication (MFA) — MFA is not enforced or prompted by the UI. This
is a Fineract server-side feature (
FINERACT_SECURITY_2FA_ENABLED); the UI does not guide operators to enable it.
- Session token theft via same-origin script: Any JavaScript executing on the same origin
(e.g., from a successful XSS attack or a compromised npm dependency) can read the auth
token from
sessionStorage. Without CSP, there is no second line of defence. config.jsonpoisoning: If an attacker can modify the staticconfig.jsonserved by NGINX, they can redirect all API calls (including login credentials) to an attacker-controlled host. This requires compromise of the web server and is treated as an environmental issue, not an application bug.localStorageAPI URL override via XSS:ConfigService.setApiUrl()writes tolocalStorage. An XSS payload can call this to redirect subsequent API calls.
What the operator / deployer must do for the assumptions in §5–§7 to hold:
-
Terminate TLS before the browser. The shipped NGINX configuration listens on HTTP port 80 only. Place a TLS-terminating reverse proxy (NGINX with a CA-trusted certificate, AWS ALB, Cloudflare, etc.) in front of the container before exposing it to any network. Without TLS, session tokens are transmitted in plaintext.
-
Set HTTP security headers on the NGINX or reverse-proxy layer. At a minimum:
Content-Security-Policy— restrictscript-srcto'self'; restrictconnect-srcto the Fineract API origin. This is the single most important header for preventing session token exfiltration via XSS.Strict-Transport-Security: max-age=31536000; includeSubDomainsX-Frame-Options: DENY(orSAMEORIGINif embedding is needed)X-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin
-
Configure
FINERACT_API_URLto a TLS-protected Fineract instance. An HTTP-only Fineract endpoint will cause the browser to transmit credentials in plaintext, even if the SPA itself is served over HTTPS. -
Do not use
npm startin production. The Angular dev server is not hardened, runs HTTP on localhost, uses a self-signed certificate, and disables TLS verification for the upstream proxy ("secure": falseinproxy.conf.json). -
Build with
--configuration production. Development builds include Angular debug tooling (ng.probe, zone tracing, verbose error messages) that increases attack surface and leaks implementation details. Always runnpm run build(which usesproductionconfiguration) for any non-development deployment. -
Rotate deployment secrets after each release. If
FINERACT_API_URLorDEFAULT_TENANTare passed as container environment variables, ensure they are managed via a secret store (e.g., Kubernetes Secrets, Vault) and not committed to image layers or source control. -
Enable Fineract 2FA (
FINERACT_SECURITY_2FA_ENABLED=true) for production admin accounts. The UI does not enforce MFA; this must be configured at the Fineract level. -
Implement Subresource Integrity (SRI) for external CDN resources loaded in
index.html(Google Fonts, Material Icons). Either self-host these assets or addintegrityandcrossoriginattributes to<link>tags. -
Scope the NGINX
Cache-Controlpolicy forconfig.jsonandindex.html. Both must not be long-cached (no-cacheorno-storerecommended) so that a redeployment with a new API URL is reflected immediately without cache poisoning. -
Run
npm audit/ Dependabot on every release cycle to detect vulnerable npm dependencies before deployment.
-
Serving the SPA over HTTP in production. The shipped NGINX config has no TLS. Operators who expose port 80 directly to users will transmit auth tokens in plaintext. Every login will be a credential in the clear.
-
Using
npm startas a production server. The Angular CLI dev server is not suitable for production: it accepts any origin, skips TLS certificate validation for upstream calls, and exposes Angular's debug utilities. -
Setting
fineractApiUrlto an HTTP (non-TLS) endpoint. Even if the SPA is served over HTTPS, settingFINERACT_API_URL=http://...will cause mixed-content blocks in modern browsers and, where allowed, transmit credentials in plaintext. -
Omitting a Content Security Policy. Without CSP, any injected
<script>(e.g., via a stored XSS in a Fineract text field rendered in the UI) can readsessionStorageand exfiltrate the auth token. -
Relying on
*appHasPermissionalone as an access control gate. The directive controls UI visibility, not API access. A determined user can call Fineract API endpoints directly (e.g., via browser DevTools) regardless of which buttons are hidden. All authorization must be enforced server-side by Fineract. -
Long-lived shared browser sessions on kiosk / shared-desk machines. The idle timeout is 15 minutes. On a shared workstation where the previous user did not explicitly log out and the tab was not closed, the
sessionStoragesession persists for the tab lifetime. Operators should enforce short browser session policies on shared machines. -
Treating
rbacEnabledas a security control. TherbacEnabledflag (§5a) toggles UI visibility only. Setting it totruehides navigation items and action buttons a user lacks permissions for, but does not prevent that user from calling the corresponding Fineract API endpoints directly. Conversely, setting it tofalsedoes not grant any additional API access. Server-side authorization in Fineract remains the sole enforcement point; the flag must never be relied upon as an access-control gate.
The following changes should trigger a revision of this threat model:
- New authentication scheme — e.g., replacing Basic Auth with OAuth2 / OIDC, adding SAML, or integrating with a Keycloak PKCE flow. Token storage, refresh, and interceptor logic would fundamentally change.
- Addition of server-side rendering (SSR) — Angular Universal or similar would add a Node.js server-side trust boundary and change where secrets and sessions are held.
- WebSocket or gRPC-Web transport — current model assumes stateless HTTP; persistent bi-directional connections introduce new session-hijacking surfaces.
- Service Worker / PWA support —
ARCHITECTURE_DECISIONS.mdnotes IndexedDB sync as a future offline feature. A service worker intercepts all outgoing requests (including those bearing credentials) and constitutes a new trust boundary. - Third-party script embedding — addition of analytics, monitoring, chat, or A/B testing scripts changes the effective CSP and expands the XSS / data-exfiltration surface.
- Cross-origin iframe embedding — if the SPA is designed to be embedded in a parent
frame,
X-Frame-Options: DENYmust be revisited and clickjacking considered. - Encrypted
localStoragevia Web Crypto API — as noted inARCHITECTURE_DECISIONS.mdas a planned improvement. If implemented, the key management strategy must be modelled. - White-label / multi-tenant branding via
branding.json— if a tenant-controlled JSON file is fetched and its values are rendered into the DOM without sanitisation, a stored XSS vector is introduced. - New CVE affecting Angular, Angular Material, or a bundled dependency that cannot be
cleanly routed to one of the §13 dispositions — this indicates a
MODEL-GAPand requires model revision.
| Disposition | Meaning | Licensed by |
|---|---|---|
VALID |
Violates a property the project claims, via an in-scope adversary and input. | §8, §6, §7 |
VALID-HARDENING |
No §8 property is violated, but the finding makes a §11 misuse significantly easier or exposes a defence-in-depth gap. Reported privately; fixed at maintainer discretion; typically no CVE. | §11 |
OUT-OF-MODEL: backend-layer |
The finding targets the Fineract API, database, or server-side logic, not the Angular SPA. Route to the Fineract server-side threat model. | §3 |
OUT-OF-MODEL: trusted-input |
Requires attacker control of a parameter the model marks as operator-trusted (e.g., config.json, container environment variables). |
§6 |
OUT-OF-MODEL: adversary-not-in-scope |
Requires an attacker capability the model excludes (e.g., physical device access, browser zero-day, compromised npm package). | §7 |
OUT-OF-MODEL: unsupported-deployment |
Only manifests under a deployment the project does not support (e.g., using the Angular dev server in production, HTTP-only deployment). | §5a, §11 |
BY-DESIGN: property-disclaimed |
Concerns a property the project explicitly does not provide (e.g., client-side rate limiting, CSP enforcement, storage encryption). | §9 |
KNOWN-NON-FINDING |
Matches a known misuse pattern or explicitly documented non-finding under §11. | §11 |
MODEL-GAP |
Cannot be cleanly routed to any of the above. Triggers §12 revision and requires model update before triage can conclude. | §12 |
Don't add any here. Bring them up in private if they are sensitive (use the ASF security reporting process described above), otherwise in chat, on the mailing list, or in the issue tracker.
The project enforces the following automated security and quality gates on every pull request
and push to main/develop via .github/workflows/ci.yml,
.github/workflows/codeql.yml, and
.github/workflows/zizmor.yml. All workflows pin action
versions to a full commit SHA and use persist-credentials: false to limit credential
exposure.
Workflow job: security (CI)
Command: npm audit --audit-level=high --omit=dev
Scans all production runtime dependencies against the npm advisory database. The
--omit=dev flag excludes build tooling (e.g., esbuild, vite) that is never shipped to
users; the --audit-level=high threshold fails the build on any high or critical
severity finding. The job runs after dependencies (which verifies npm ci integrity) so
that the lockfile state is guaranteed to match what was installed.
What it catches: Known CVEs in Angular, Angular Material, ngx-translate, and any other production npm package declared in
package.json.
What it does not catch: Vulnerabilities introduced at the OS or container layer; zero-day advisories not yet published to the npm registry; dev-dependency vulns (intentionally excluded — they are never deployed).
Workflow jobs: lint (TypeScript files), html-lint (Angular templates) — CI
Commands: npm run lint, npx eslint "src/**/*.html"
Config: eslint.config.js, sonar-project.properties
TypeScript sources are linted with eslint, typescript-eslint (strict + stylistic),
angular-eslint, and eslint-plugin-sonarjs (SonarJS recommended ruleset). Angular
templates are linted separately for structural correctness and accessibility rules
(angular-eslint/template-accessibility). The generated src/app/api/ directory is
excluded from linting.
Key security-relevant rules enforced:
| Rule | Enforced by | Catches |
|---|---|---|
sonarjs/no-duplicate-string |
SonarJS | Accidental duplication that can hide typo-based injection points |
@angular-eslint/no-output-on-prefix |
angular-eslint | Misnaming that can confuse data-binding direction |
| Template accessibility rules | angular-eslint template | Missing alt, for, ARIA misuse — reduces social-engineering surface via deceptive UI |
| Recommended TypeScript strict rules | typescript-eslint | Unsafe casts, any leakage, implicit returns that can mask auth failures |
ESLint results are also reported to SonarQube via sonar.eslint.reportPaths=eslint-report.json
for continuous quality tracking across branches.
Workflow: codeql.yml
Trigger: push/PR to main; weekly scheduled scan (Thursdays 11:40 UTC)
Languages analysed: javascript-typescript, actions
GitHub CodeQL performs deep static analysis on the TypeScript/JavaScript source and on the
GitHub Actions workflow files themselves. Results are uploaded to GitHub's Security tab as
SARIF and require security-events: write permission. The actions language target catches
workflow-level security issues such as script injection via untrusted inputs, missing
permission scopes, and dangerous run: invocations.
What it catches: DOM-based XSS patterns,
eval/innerHTMLmisuse, prototype pollution, path traversal in Node.js scripts, insecure regular expressions (ReDoS), CI/CD workflow injection.
What it does not catch: Runtime/logic-level authorization flaws; backend API vulnerabilities (out of scope for a frontend SAST tool).
Workflow: zizmor.yml
Trigger: push/PR on any branch when .github/workflows/** files change
Tool: zizmor v0.5.7 (pinned by SHA)
Zizmor audits the Actions workflow YAML files themselves for security misconfigurations.
It runs at min-severity: informational / min-confidence: low to surface even advisory-
level findings. Results are written to GitHub's Security tab via SARIF
(security-events: write). Online audits are disabled (online-audits: false) so the
tool does not exfiltrate workflow content to external services.
What it catches: Script injection from
github.event.*context variables, excessive workflow permissions, unpinned action references, dangerouspull_request_targetpatterns.
Config: .github/dependabot.yml
Schedule: weekly, with a 7-day cooldown between updates
Ecosystems: npm (application dependencies), github-actions (workflow actions)
Dependabot opens automated PRs to update npm packages and pinned GitHub Action SHAs.
Every update PR triggers the full CI suite (including npm audit and CodeQL), meaning
dependency updates are validated before merge. Cooldown prevents update flood while still
ensuring timely patch uptake.
Workflow job: rat-scan (CI)
Tool: Apache RAT 0.17 (SHA-256 verified before execution)
Scans every file in the repository against the exclusion list in .rat-excludes
and fails if any file is missing an Apache License 2.0 header. The JAR is checksum-verified
at download time to prevent supply-chain tampering of the scanning tool itself.
Security relevance: Ensures no unlicensed third-party code is silently introduced; provides an audit trail of all file additions.
Workflow job: compliance (CI)
Command: npx license-checker --production --onlyAllow "MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD"
Verifies that every production npm dependency uses an OSI-approved permissive licence.
Dependencies with copyleft (GPL, LGPL, AGPL) or proprietary licences are rejected. The
step also runs scripts/check-license.sh to enforce
per-file Apache headers within the project's own source code.
Workflow job: format (CI)
Command: npm run format:check
Enforces consistent code formatting via Prettier. While primarily a quality gate, uniform formatting reduces the risk of "invisible character" or whitespace-based obfuscation tricks being introduced undetected in diffs.
All contributors — human or AI-assisted — must satisfy the following before a PR can merge.
| Responsibility | How to satisfy it |
|---|---|
No new HIGH/CRITICAL npm advisories |
Run npm audit --audit-level=high --omit=dev locally before pushing; address or document any findings. |
| No ESLint errors | Run npm run lint and npx eslint "src/**/*.html" locally; fix all errors. SonarJS warnings must also be resolved. |
No bypassSecurityTrust* usage |
Angular's DomSanitizer bypass APIs must not be introduced. If unavoidable, open a security discussion first. |
No innerHTML / eval / document.write |
Use Angular template binding instead. CodeQL will flag these; CI will fail. |
| Apache License 2.0 header on every new file | Copy the header block from any existing source file. RAT scan will fail without it. |
| No hard-coded credentials, URLs, or tenant IDs | Use ConfigService / environment files / container env vars. |
| Translation keys for all user-facing strings | Run npm run i18n:check to verify no key is missing or unused. |
| Server-side pagination for new data grids | Per ARCHITECTURE_DECISIONS.md; loading unbounded result sets is a denial-of-service vector. |
PRs that touch the files below require explicit maintainer sign-off on the security implications before merge:
| File / area | Why |
|---|---|
auth.interceptor.ts |
Controls how auth tokens and tenant headers are injected; regression here breaks all API authentication. |
auth.service.ts |
Session storage, token handling, permission checks, and the ALL_FUNCTIONS bypass gate. |
auth.guard.ts |
Route-level authentication gate; any weakening lets unauthenticated users access back-office routes. |
has-permission.directive.ts |
UI-level RBAC enforcement; a bug here exposes privileged actions to lower-privilege users. |
config.service.ts |
Controls the API base URL written to localStorage; an XSS-reachable path if misused. |
idle.service.ts |
Session timeout logic; weakening allows indefinite unattended sessions. |
error.interceptor.ts |
Error message rendering; must not reflect raw server responses into the DOM. |
app.config.ts |
Interceptor chain registration and BASE_PATH initialisation; order matters for security. |
.github/workflows/ |
Any workflow change is also audited by Zizmor and CodeQL (actions language). |
- When you add a new feature that changes the attack surface (see §12 for the trigger list), update this document in the same PR.
- When CI flags a finding you believe is a false positive, document the rationale in the PR description. Do not suppress linter rules or audit advisories silently.
- When a new CVE in Angular or a major dependency is published, open a tracking issue within 48 hours and reference the relevant §13 disposition.
This is an Apache Software Foundation project. Every file and every dependency must be compatible with the ASF Licensing Policy before it can be merged. This section describes the three-layer enforcement model used to prevent non-ASF-compliant material from entering the repository.
Who runs it: CI (compliance job via scripts/check-license.sh);
CI (rat-scan job via Apache RAT 0.17).
Every project-owned source file must carry the full Apache License 2.0 SPDX header block. Two independent tools verify this on every PR:
The custom script scans the following file types under src/, deploy/, .github/, and
scripts/:
| Extension / filename | Checked for |
|---|---|
*.ts, *.html, *.scss |
Licensed to the Apache Software Foundation string |
*.yml, *.sh, Dockerfile, nginx.conf |
same |
eslint.config.js, .prettierignore |
same |
Root-level *.json (except package-lock.json, angular.json) |
Apache-2.0 or Apache License string |
src/assets/i18n/*.json |
same |
The generated API client (src/app/api/) is explicitly excluded — it is regenerated from
the upstream Fineract OpenAPI spec and carries its own provenance.
If any file is missing a header the script exits non-zero and CI fails.
To add a header to a new file, copy the exact block from any existing .ts file
(for TypeScript/SCSS/JS) or any existing .yml file (for YAML/shell):
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
Verify locally before pushing:
./scripts/check-license.shRAT scans the entire repository tree (not just source extensions) and fails on any file
that lacks a recognised licence header, unless it appears in .rat-excludes.
Files legitimately excluded from RAT (lockfiles, generated assets, binary images, IDE
configs, spec files, and third-party API definitions) are listed in .rat-excludes. Every
entry in that file must be justified; do not add paths to silence RAT without a documented
reason.
The RAT JAR is downloaded fresh on each CI run and its SHA-256 digest is verified before execution to prevent supply-chain tampering of the scanning tool:
401939ebe5a52c6ed524029897bf914eaaba503d36c069ebcdbd8847a9e7cf93 apache-rat-0.17.jar
Who runs it: CI (compliance job) and any contributor locally before adding a package.
Command: npx --no-install license-checker --production --onlyAllow "MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD"
license-checker is declared as a devDependency in package.json
(line 64: "license-checker": "^25.0.1"). This means it is available to every developer
immediately after npm install — no separate install step is needed.
The --onlyAllow flag is the hard gate: if any package in the resolved dependency tree
carries a licence not in the approved list, the command exits with a non-zero code and
prints the offending package. This makes the check runnable identically on a developer
laptop and in CI, with the exact same result.
Before adding any new npm dependency, verify it passes the licence check locally:
# 1. Install the new package normally
npm install <package-name>
# 2. Check all production deps still pass the allowlist
npx --no-install license-checker --production \
--onlyAllow "MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD"If the check fails you will see output like:
ENOENT: /path/to/project/node_modules/<package>
licenses: GPL-3.0
...
Found packages with incompatible licenses!
Remove the package and find an ASF-compatible alternative before committing
package.json or package-lock.json.
Only the following identifiers are permitted for production (dependencies) entries:
| SPDX identifier | ASF category |
|---|---|
Apache-2.0 |
Category A — free to use |
MIT |
Category A — free to use |
BSD-2-Clause |
Category A — free to use |
BSD-3-Clause |
Category A — free to use |
ISC |
Category A — free to use |
0BSD |
Category A — free to use |
The following are blocked — adding a dependency with any of these licences will fail both the local check and CI:
| Blocked family | Examples | ASF category | Reason |
|---|---|---|---|
| Copyleft (strong) | GPL-2.0, GPL-3.0, AGPL-3.0 |
Category X | Incompatible with ALv2; cannot be bundled |
| Copyleft (weak) | LGPL-2.0, LGPL-2.1, LGPL-3.0 |
Category X | Incompatible when statically linked |
| Non-commercial | CC-BY-NC-* |
Category X | Restricts commercial use |
| Proprietary / EULA | various | Category X | Cannot be distributed under ALv2 |
| Creative Commons (unversioned) | CC-BY-1.0, CC-BY-2.0 |
Category B | Docs only; must not appear in dependencies |
Caution
If license-checker fails after adding a dependency, do not add the package to an
allowlist override. Instead, find an alternative library with an approved licence,
or raise a discussion on the mailing list for a formal ASF legal exception.
The --production flag restricts the scan to the dependencies block of package.json
only. devDependencies (TypeScript compiler, Karma, Prettier, Playwright, etc.) are
excluded because they are never bundled into the production build served to users.
However, contributors should still prefer Category A licences for dev tooling as a matter
of good hygiene.
Config: .github/dependabot.yml
Weekly automated PRs ensure no approved dependency silently re-licenses itself in a later
version. When Dependabot opens a version bump, the compliance job re-runs license-checker
against the updated lockfile before merge is permitted.
.asf.yaml enforces the following on the main branch at the GitHub
repository level:
| Rule | Setting |
|---|---|
| Signed commits required | required_signatures: true |
| Force push prohibited | restrict_force_push: true |
| Branch deletion prohibited | restrict_deletion: true |
| Minimum approving reviews | 1 (required_approving_review_count: 1) |
| Conversations must be resolved | required_conversation_resolution: true |
| Squash and rebase merges disabled | Only merge commits permitted |
These controls mean a non-compliant file cannot be force-pushed onto main after a
compliant history is established, and every merge requires at least one human reviewer
to have approved the diff — including any licence header present or absent.
| Failing job | Most likely cause | Fix |
|---|---|---|
compliance (check-license.sh) |
New *.ts/*.html/*.scss/*.yml file missing ASF header |
Add the header block from any existing file |
compliance (check-license.sh) |
New root-level *.json missing "license": "Apache-2.0" field |
Add the field to the JSON file |
rat-scan |
File not in .rat-excludes and no header recognised by RAT |
Either add the ASF header or add a justified entry to .rat-excludes |
compliance (license-checker) |
New npm dependency uses a blocked licence | Replace the dependency or raise an ASF legal exception request |
compliance (license-checker) |
Transitive dep upgraded to a blocked licence | Pin the previous version, open an issue, alert the mailing list |
A sidecar threat-model.yaml is recommended for automated triage pipelines. The prose
document remains canonical; the sidecar is a derived index.
# threat-model.yaml (stub — expand as needed)
schema_version: '1.0'
document: security.md
component: fineract-backoffice-ui
layer: browser-spa
framework: angular@21
repo: https://github.com/apache/fineract-backoffice-ui
trust_boundaries:
- id: browser_sandbox
description: Angular SPA executing in browser JS engine
- id: config_json
description: Static config file served by NGINX; operator-controlled
- id: session_storage
description: Tab-scoped browser storage holding auth token
- id: local_storage
description: Origin-scoped browser storage holding tenant ID and runtime config
security_properties_provided:
- route_authentication_guard
- automatic_auth_header_injection
- ui_level_rbac_via_has_permission
- session_scoped_token_storage
- idle_session_timeout_15min
- correlation_id_tracing
- angular_template_xss_protection
- structured_error_message_rendering
security_properties_not_provided:
- content_security_policy
- http_security_headers
- tls_on_nginx_listener
- client_side_rate_limiting
- route_level_rbac_beyond_auth_guard
- storage_encryption
- ui_audit_log
- subresource_integrity_for_cdn_assets
- token_refresh_rotation
- mfa_enforcement
triage_dispositions:
- VALID
- VALID-HARDENING
- OUT-OF-MODEL: backend-layer
- OUT-OF-MODEL: trusted-input
- OUT-OF-MODEL: adversary-not-in-scope
- OUT-OF-MODEL: unsupported-deployment
- BY-DESIGN: property-disclaimed
- KNOWN-NON-FINDING
- MODEL-GAP
ci_enforced_controls:
- tool: npm-audit
workflow: ci.yml
job: security
command: 'npm audit --audit-level=high --omit=dev'
scope: production-dependencies-only
- tool: eslint
workflow: ci.yml
jobs: [lint, html-lint]
plugins: [typescript-eslint, angular-eslint, sonarjs]
sonar_report: eslint-report.json
- tool: github-codeql
workflow: codeql.yml
languages: [javascript-typescript, actions]
trigger: [push-main, pr-main, weekly-schedule]
- tool: zizmor
workflow: zizmor.yml
scope: github-actions-workflows
trigger: workflow-file-changes
- tool: dependabot
config: .github/dependabot.yml
ecosystems: [npm, github-actions]
schedule: weekly
- tool: apache-rat
workflow: ci.yml
job: rat-scan
version: '0.17'
checksum_verified: true
- tool: license-checker
workflow: ci.yml
job: compliance
allowed: [MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD]
- tool: prettier
workflow: ci.yml
job: format
command: 'npm run format:check'
licence_compliance:
policy: ASF Licensing Policy (https://www.apache.org/legal/resolved.html)
layers:
- layer: 1
name: source-file-headers
tools:
- name: check-license.sh
path: scripts/check-license.sh
ci_job: compliance
scans: [src, deploy, .github, scripts]
excludes: [src/app/api]
- name: apache-rat
version: '0.17'
ci_job: rat-scan
exclude_file: .rat-excludes
sha256_verified: true
- layer: 2
name: dependency-licence-allowlist
tool: license-checker
ci_job: compliance
flag: --production
allowed_spdx: [MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD]
blocked_categories: [Category-X-copyleft, Category-X-non-commercial, proprietary]
- layer: 3
name: dependabot-relicensing-guard
config: .github/dependabot.yml
schedule: weekly
re_runs_compliance_on_update: true
branch_protection:
config: .asf.yaml
branch: main
required_signatures: true
restrict_force_push: true
restrict_deletion: true
required_approving_review_count: 1
required_conversation_resolution: true
allowed_merge_types: [merge]