Skip to content

Latest commit

 

History

History
1019 lines (792 loc) · 60.5 KB

File metadata and controls

1019 lines (792 loc) · 60.5 KB

Security Policy

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.

Reporting a Vulnerability

To report a new vulnerability you have discovered please follow the ASF vulnerability reporting process.


Fineract Backoffice UI — Threat Model

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.

§1 Header

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.


§2 Scope and intended use

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.

Primary intended use cases

  • 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 *appHasPermission structural directive.
  • Multi-tenant operation: A single deployed instance serves multiple Fineract tenants, differentiated by a Fineract-Platform-TenantId header injected by authInterceptor on every outgoing request.

Deployment contexts

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.

Caller roles (UI-level)

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.

§3 Out of scope (explicit non-goals)

Components not modelled here

  • 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.

Threats the project does not attempt to defend against

  • 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.

§4 Trust boundaries and data flow

Where the trust boundary sits

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.

Data flow

[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)]

Trust transitions

  1. 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.
  2. Browser → Fineract API: TLS-encrypted. The Angular authInterceptor attaches Authorization: Basic <token> and tenant headers to every API request. The token is the base64EncodedAuthenticationKey returned by the Fineract /authentication endpoint and stored in sessionStorage.
  3. Container start → config.json: The entrypoint.sh writes FINERACT_API_URL and DEFAULT_TENANT environment 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 via ConfigService.loadConfig().

§5 Assumptions about the environment

Browser / runtime

  • 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.

Web server

  • 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) per nginx.conf; index.html must not be cached for longer than the deployment cycle.

Angular framework security

  • Angular's built-in XSS protections (automatic HTML escaping via the template engine, DomSanitizer for dynamic content) are not bypassed by application code. Use of bypassSecurityTrust* APIs is treated as a VALID-HARDENING trigger.

Network

  • All browser-to-API communication is over TLS in production.
  • The Angular dev server's proxy.conf.json sets "secure": false (skips TLS verification for the upstream Fineract connection). This is a dev-only convenience and must not be replicated in production infrastructure.

Concurrency

  • The currentTenantId signal in AuthService is initialised from localStorage at 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).

§5a Build-time and configuration variants

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.

RBAC feature flag (rbacEnabled)

The rbacEnabled flag gates two structural directives and the sidebar's navigation filtering:

  • *appHasPermission (src/app/shared/directives/has-permission.directive.ts) — checks AuthService.hasPermission(). When rbacEnabled === false, it renders unconditionally.
  • *appInstitutionFeature (src/app/shared/directives/has-institution-feature.directive.ts) — checks InstitutionConfigService.isFeatureEnabled() for group-lending features (groups, centers, collection_sheet). When rbacEnabled === 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.


§6 Assumptions about inputs

Input sources

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.

Size and rate

  • 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.

§7 Adversary model

Who is in scope

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.

Who is explicitly out of scope

  • 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.

§8 Security properties the project provides

  • Route-level authentication enforcementauthGuard redirects unauthenticated users to /login for all protected routes. The SPA never renders back-office content before a valid session is established.
  • Automatic session injectionauthInterceptor attaches Authorization: Basic <token> and tenant headers to every outgoing API request, preventing accidental unauthenticated calls.
  • UI-level RBAC*appHasPermission structural directive dynamically shows or hides UI elements based on the granular permission list returned by Fineract at login. The ALL_FUNCTIONS / ALL_FUNCTIONS_READ super-permissions are correctly short-circuited.
  • Session scoped to browser tab — the authentication token (base64EncodedAuthenticationKey) is stored in sessionStorage (tab-scoped), not localStorage (origin-scoped). Closing the tab clears the session.
  • Automatic idle logoutIdleService forces logout after 15 minutes of inactivity, with a 2-minute warning dialog. This limits the window of exposure for unattended sessions.
  • Correlation ID tracingcorrelationIdInterceptor adds a X-Correlation-ID UUID 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, or eval usages exist in current application code.
  • Runtime API URL configurationConfigService fetches config.json at bootstrap and caches the result, preventing hard-coded API URL issues across environments.
  • Error message sanitisationerrorInterceptor extracts structured error fields (defaultUserMessage, developerMessage, parameterName) from API responses rather than reflecting raw response bodies into the DOM.

§9 Security properties the project does not provide

  • Content Security Policy (CSP) — no Content-Security-Policy header is set by the shipped nginx.conf. The operator must add it. Without CSP, injected scripts can read sessionStorage and exfiltrate auth tokens.
  • HTTP security headersnginx.conf does not include X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, or Referrer-Policy. These must be added by the operator.
  • TLS on the NGINX listener — the shipped nginx.conf listens 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) — the authGuard only checks isAuthenticated(). 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 / localStorage encryption — auth tokens and config are stored in plaintext browser storage. Any script on the same origin can read them. (ARCHITECTURE_DECISIONS.md notes 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.html without integrity attributes, making them a potential supply-chain injection vector if the CDN is compromised.
  • Token refresh / rotation — the base64EncodedAuthenticationKey issued 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.

Known attack classes this project cannot defend against

  • 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.json poisoning: If an attacker can modify the static config.json served 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.
  • localStorage API URL override via XSS: ConfigService.setApiUrl() writes to localStorage. An XSS payload can call this to redirect subsequent API calls.

§10 Downstream responsibilities

What the operator / deployer must do for the assumptions in §5–§7 to hold:

  1. 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.

  2. Set HTTP security headers on the NGINX or reverse-proxy layer. At a minimum:

    • Content-Security-Policy — restrict script-src to 'self'; restrict connect-src to the Fineract API origin. This is the single most important header for preventing session token exfiltration via XSS.
    • Strict-Transport-Security: max-age=31536000; includeSubDomains
    • X-Frame-Options: DENY (or SAMEORIGIN if embedding is needed)
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: strict-origin-when-cross-origin
  3. Configure FINERACT_API_URL to 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.

  4. Do not use npm start in 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": false in proxy.conf.json).

  5. 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 run npm run build (which uses production configuration) for any non-development deployment.

  6. Rotate deployment secrets after each release. If FINERACT_API_URL or DEFAULT_TENANT are 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.

  7. 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.

  8. Implement Subresource Integrity (SRI) for external CDN resources loaded in index.html (Google Fonts, Material Icons). Either self-host these assets or add integrity and crossorigin attributes to <link> tags.

  9. Scope the NGINX Cache-Control policy for config.json and index.html. Both must not be long-cached (no-cache or no-store recommended) so that a redeployment with a new API URL is reflected immediately without cache poisoning.

  10. Run npm audit / Dependabot on every release cycle to detect vulnerable npm dependencies before deployment.


§11 Known misuse patterns

  1. 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.

  2. Using npm start as 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.

  3. Setting fineractApiUrl to an HTTP (non-TLS) endpoint. Even if the SPA is served over HTTPS, setting FINERACT_API_URL=http://... will cause mixed-content blocks in modern browsers and, where allowed, transmit credentials in plaintext.

  4. 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 read sessionStorage and exfiltrate the auth token.

  5. Relying on *appHasPermission alone 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.

  6. 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 sessionStorage session persists for the tab lifetime. Operators should enforce short browser session policies on shared machines.

  7. Treating rbacEnabled as a security control. The rbacEnabled flag (§5a) toggles UI visibility only. Setting it to true hides 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 to false does 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.


§12 Conditions that would change this model

The following changes should trigger a revision of this threat model:

  1. 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.
  2. 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.
  3. WebSocket or gRPC-Web transport — current model assumes stateless HTTP; persistent bi-directional connections introduce new session-hijacking surfaces.
  4. Service Worker / PWA supportARCHITECTURE_DECISIONS.md notes IndexedDB sync as a future offline feature. A service worker intercepts all outgoing requests (including those bearing credentials) and constitutes a new trust boundary.
  5. 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.
  6. Cross-origin iframe embedding — if the SPA is designed to be embedded in a parent frame, X-Frame-Options: DENY must be revisited and clickjacking considered.
  7. Encrypted localStorage via Web Crypto API — as noted in ARCHITECTURE_DECISIONS.md as a planned improvement. If implemented, the key management strategy must be modelled.
  8. 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.
  9. 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-GAP and requires model revision.

§13 Triage dispositions

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

§14 Open questions for the maintainers

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.


§15 CI-enforced security controls

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.

Security Audit — npm audit

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).

ESLint with SonarJS rules

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.

GitHub CodeQL (SAST)

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/innerHTML misuse, 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).

Zizmor — GitHub Actions workflow auditing

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, dangerous pull_request_target patterns.

Dependabot — automated dependency updates

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.

Apache RAT scan — license header enforcement

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.

License compatibility check

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.

Prettier formatting check

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.


§16 Contributor responsibilities

All contributors — human or AI-assisted — must satisfy the following before a PR can merge.

Every PR

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.

Security-sensitive areas — extra review required

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).

Threat model maintenance

  • 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.

§17 Licence compliance

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.


Layer 1 — Source file headers (check-license.sh + Apache RAT)

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:

scripts/check-license.sh

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.sh

Apache RAT 0.17

RAT 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

Layer 2 — npm dependency licence restriction (license-checker)

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"

How the npm-level restriction works

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.

Run it locally before opening a PR

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.

Approved SPDX identifiers

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

Blocked licence families

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.

Scope: --production flag

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.


Layer 3 — Dependabot keeps approved dependencies current

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 branch protection (asf.yaml)

.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.


Quick-reference: what to do when CI fails

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

§18 Machine-readable companion

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]