From 5291508aa4e423e494714d606fd30dd51250b07a Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Tue, 11 Aug 2026 10:34:12 +0300 Subject: [PATCH 01/11] initial draft of the threat model --- docs/security/review-template.md | 90 ++++++++++++++ docs/security/threat-model.md | 197 +++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 docs/security/review-template.md create mode 100644 docs/security/threat-model.md diff --git a/docs/security/review-template.md b/docs/security/review-template.md new file mode 100644 index 00000000..d1a8c393 --- /dev/null +++ b/docs/security/review-template.md @@ -0,0 +1,90 @@ +# Security review record — `` `` + +> Copy this file to `review--.md` for each release under review, fill it +> in, and merge it. It is the second artifact Microsoft requires alongside +> [threat-model.md](threat-model.md). `IgniteUI.Blazor.Lite` and +> `IgniteUI.Blazor.Templates` are reviewed and recorded separately. + +| | | +|---|---| +| **Package / version** | | +| **Commit reviewed** | | +| **Review date** | | +| **Threat model version** | | +| **Outcome** | | + +## Reviewers + +At least one reviewer must not be an author of the code under review. + +| Name | Role | Author of reviewed code? | +|---|---|---| +| | | | + +## Coverage + +Tick what was actually performed; an unticked row is a stated limitation, not an omission. + +- [ ] Threat model walkthrough against the current code +- [ ] Manual review of the JS interop surface (`src/componentsBase/WebViewCallback.cs`, `BaseRendererControl`, `RendererSerializer`) +- [ ] Manual review of the unmarshalled path (`src/componentsBase/RuntimeHelper.cs`, `UnmarshalledDataSource`) +- [ ] Manual review of the serialization boundary (`JsonDataSource`, `RendererSerializer`) +- [ ] Rendering path review (`lit-html` / `igniteui-webcomponents` usage of `unsafeHTML`) +- [ ] Dependency review (`package-lock.json`, `Directory.Packages.props`) +- [ ] Static analysis results reviewed (CodeQL `csharp` + `javascript`) +- [ ] Build and release pipeline review (`.github/workflows/`) +- [ ] Package content inspection for **both** `.nupkg` files, including the template pack +- [ ] Scaffolded-app secure-defaults checklist executed against `dotnet new` +- [ ] Consumer-facing security documentation reviewed for accuracy + +## Findings register — `IgniteUI.Blazor.Lite` + +| ID | Summary | Sev | Disposition | Evidence / justification | +|---|---|---|---|---| +| TM-IX-01 | `WebCallback` public; client-supplied `containerId` addresses any control | High | | | +| TM-IX-02 | `OnInvokeReturn` accepts untyped `object` | Medium | | | +| TM-IX-03 | `AdjustDynamicContentBatch` deserializes a client-supplied batch | Medium | | | +| TM-IX-04 | `_controlsMap` registration is unvalidated; `Add` throws on duplicate keys | Medium | | | +| TM-IX-05 | Untrusted event args reach consumer handlers | High | | | +| TM-MEM-01 | `unsafe` + reflected `InvokeUnmarshalled` + raw WASM-heap pointers | High | | | +| TM-MEM-02 | Silent loss of the unmarshalled fast path on runtime change | Low | | | +| TM-SER-01 | Full object graph serialized to the client | High | | | +| TM-SER-02 | Prerendered state embedded in initial HTML | Low | Accepted | Inherent to Blazor SSR; app-level cache headers | +| TM-DOM-01 | Rendering path: text vs. markup (`unsafeHTML` usage) | TBD | | | +| TM-DOM-02 | Consumer `RenderFragment` templates render consumer markup | Low | By design | Razor escapes by default; `MarkupString` is an explicit opt-in | +| TM-SC-01 | Bundled third-party JS is not independently patchable | Medium | By design | Covered by the `SECURITY.md` SLAs | +| TM-SC-02 | No CodeQL/SCA/`npm audit`/dependency-review gate | High | | | +| TM-BLD-01 | Undefined `BUILD_CONFIGURATION` in release signing/validation paths | Medium | | | +| TM-BLD-02 | Signature gate passes on an empty DLL result set | Medium | | | +| TM-BLD-03 | `Nullable` disabled on the Lite project | Low | Accepted | Generated sources are unannotated | + +## Findings register — `IgniteUI.Blazor.Templates` + +| ID | Summary | Sev | Disposition | Evidence / justification | +|---|---|---|---|---| +| TM-PKG-01 | `NoDefaultExcludes=true` + broad `Content Include` packs unintended files | High | | | +| TM-PKG-02 | `test-templates.ps1` / `.sh` may land under `content/` | Medium | | | +| TM-TPL-01 | Insecure defaults replicated into every scaffolded app | High | | | +| TM-TPL-02 | Template-pinned package versions go stale | Medium | | | +| TM-TPL-03 | `0.0.1` hard-coded rather than tag-driven | Low | | | + +**Disposition values** — `Fixed` (code changed, link the PR) · `Mitigated` (compensating +control, name it) · `Accepted` (residual risk, requires an approver in the table below). + +## Accepted risks + +Every `Accepted` disposition above needs a named approver here. + +| ID | Justification | Approver | Date | +|---|---|---|---| +| | | | | + +## Release gate + +- [ ] No finding of severity **High** or above is left `Open` +- [ ] Every `Accepted` risk has a named approver +- [ ] TM-DOM-01 has a definitive answer +- [ ] `threat-model.md` has been updated to reflect this review + +**Statement:** diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md new file mode 100644 index 00000000..f5722d5b --- /dev/null +++ b/docs/security/threat-model.md @@ -0,0 +1,197 @@ +# Threat model — IgniteUI.Blazor.Lite and IgniteUI.Blazor.Templates + +| | | +|---|---| +| **Status** | Draft — awaiting maintainer review | +| **Packages in scope** | `IgniteUI.Blazor.Lite` (net8.0 / net9.0 / net10.0), `IgniteUI.Blazor.Templates` (netstandard2.0, `PackageType=Template`) | +| **Repository** | https://github.com/IgniteUI/igniteui-blazor | +| **Reviewed commit** | | +| **Document owner** | | +| **Last updated** | 2026-08-11 | +| **Method** | STRIDE per trust-boundary, mapped to Microsoft's Blazor threat-mitigation guidance | + +## 1. Why this document exists + +Microsoft requires a maintained security/threat model and a completed security review +before a third-party Blazor component package can be endorsed alongside their own +components. This document is the threat model half of that requirement. It is a **living +document**: it is updated whenever the JS interop surface, the unmarshalled data path, the +bundled third-party JavaScript, or the template content changes. + +It is not a penetration test, not an audit, and not an attestation of security. + +The two packages have **different threat classes** and are modelled separately: +`IgniteUI.Blazor.Lite` is a runtime interop surface; `IgniteUI.Blazor.Templates` is a +supply-chain and secure-defaults surface. + +## 2. Scope + +**In scope** + +- `IgniteUI.Blazor.Lite`: managed code under `src/` (notably `src/componentsBase/`) and the + webpack bundle produced from `src/src/` (`igniteui-webcomponents`, `igniteui-core`, + `lit-html`) plus the themes copied into `src/wwwroot/`. +- `IgniteUI.Blazor.Templates`: the `dotnet new` template content under + `templates/IgniteUI.Blazor.Templates/templates/` and how it is packed. +- The build and release pipelines that produce and sign both packages. + +**Out of scope** + +- The consuming application, and applications generated from the templates once the + developer has modified them. +- Internal implementation of `igniteui-webcomponents` / `igniteui-core` / `lit-html` — + trusted-but-verified dependencies; their behaviour at the rendering boundary *is* in + scope (TM-DOM-01). +- The ASP.NET Core Blazor framework. Framework guarantees are assumptions (§5). +- Storybook stories, tests and samples. + +## 3. Architecture and trust boundaries + +```mermaid +flowchart LR + subgraph SRV["Server circuit / WASM runtime — trusted"] + B["BaseRendererControl
component wrappers"] + W["WebCallback
[JSInvokable] surface"] + R["RuntimeHelper
unsafe / InvokeUnmarshalled"] + A["Consuming app
event handlers, templates"] + end + subgraph BR["Browser — untrusted"] + L["webpack bundle
Loader / ComponentRenderer"] + E["igniteui-webcomponents
+ lit-html (shadow DOM)"] + end + B -- "TB1: RendererSerializer / JsonDataSource" --> L + R -- "TB1b: unmarshalled column buffers (WASM only)" --> L + L -- "TB2: invokeMethodAsync(containerId, ...)" --> W + W --> A + L --> E +``` + +Three boundaries: + +- **TB1 — server → client.** Component state and bound data serialized to the browser. +- **TB1b — managed → WASM heap.** The unmarshalled fast path; a *memory-safety* boundary, + not just a trust boundary. Unique to this package. +- **TB2 — client → server.** Attacker-controlled input. Per Microsoft's guidance, *"Treat + any .NET method exposed to JavaScript as you would a public endpoint to the app."* + +## 4. Assets and security objectives + +| Asset | Objective | +|---|---| +| Consumer data bound to components | Confidentiality — only intended fields reach the browser | +| The Blazor circuit and the WASM heap | Availability and memory integrity | +| The consuming app's browser origin | Integrity — components never introduce script execution | +| The two published NuGet packages | Integrity — signed, reproducible, no unintended content | +| Applications scaffolded from the templates | Integrity — secure-by-default starting posture | + +## 5. Assumptions and consumer responsibilities + +| # | Assumption | +|---|---| +| A1 | The consuming app enforces authentication/authorization; components perform none. | +| A2 | The consuming app enforces a Content Security Policy appropriate to its render mode. | +| A3 | The consuming app is free of XSS. Most TB2 threats require attacker script in the page; per Microsoft's guidance an XSS-compromised client can already forge interop calls. The library's obligation is to avoid *causing* XSS and to avoid *widening* the blast radius. | +| A4 | Data bound to components has already passed the app's authorization filter. | +| A5 | Framework limits (`CircuitOptions`, `MaximumReceiveMessageSize`, interop call timeout) are left at or below their defaults. | +| A6 | Developers using the templates review and adapt the generated security configuration before production deployment. | + +## 6. Threats — `IgniteUI.Blazor.Lite` + +Severity is the residual severity **given** A1–A6. Status: `Open`, `Mitigated`, +`By design`, `Accepted`, `Verified — no finding`. + +### TB2 — client → server (JS interop callbacks) + +| ID | Threat | STRIDE | Sev | Status | +|---|---|---|---|---| +| **TM-IX-01** | `WebCallback` is a **public** class whose `[JSInvokable]` methods (`OnReady`, `OnInvokeReturn`, `OnRaiseEvent`, `AdjustDynamicContent`, `AdjustDynamicContentBatch`) all take a **client-supplied `containerId`** used as a key into a process-wide `_controlsMap`. A caller that reaches the reference can address *any* registered control in the circuit, not only the one it legitimately owns — event raising and dynamic-content mutation can be driven cross-instance. This is the largest single item in the model. | S, T, E | **High** | **Open** | +| **TM-IX-02** | `OnInvokeReturn` accepts `object returnValue` — an untyped, polymorphic value deserialized from the client and passed on to `control.OnInvokeReturn`. Weakest input contract in the surface. | T, E | Medium | **Open** | +| **TM-IX-03** | `AdjustDynamicContentBatch` deserializes a client-supplied `batch` string into a dictionary array and iterates it, driving render-tree mutation from untrusted input. | T, D | Medium | **Open** | +| **TM-IX-04** | `_controlsMap` is keyed by `ContainerId` and populated via `Register`, with no validation that the caller is entitled to that key, and `Add` (not indexer assignment) will throw on a duplicate key. | S, D | Medium | **Open** | +| **TM-IX-05** | Untrusted event args flow into consumer event handlers. If the app forwards them into dynamic LINQ, SQL or reflection, this becomes injection. | T, E | High *(consumer-facing)* | **Open** — needs documentation | + +### TB1b — memory safety (WASM unmarshalled path) + +| ID | Threat | STRIDE | Sev | Status | +|---|---|---|---|---| +| **TM-MEM-01** | `RuntimeHelper` reflection-discovers `InvokeUnmarshalled` on the WASM runtime, builds a delegate with `Expression.Compile()`, and invokes it from `unsafe` methods passing `UnmarshalledColumn[]` — raw pointers into the WASM heap. `AllowUnsafeBlocks=true`. A mismatch between the managed layout and the JS-side reader is a memory-corruption / type-confusion condition rather than a normal exception. | T, E | **High** | **Open** — needs justification or scope limit | +| **TM-MEM-02** | The unmarshalled API is deprecated and reached only by reflection, so a runtime change silently disables the fast path. Behaviour then diverges between runtimes with no signal. | R | Low | **Open** | +| **TM-MEM-03** | `Expression.Compile()` requires a JIT and is incompatible with full AOT/trimming. A compatibility constraint rather than a vulnerability, recorded here because it constrains the mitigation options for TM-MEM-01. | — | — | **Note** | + +### TB1 — server → client (serialization and rendering) + +| ID | Threat | STRIDE | Sev | Status | +|---|---|---|---|---| +| **TM-SER-01** | Bound data is serialized to the browser through `JsonDataSource` / `RendererSerializer`. Consumers binding ORM entities ship every property — including PII and internal fields — to the client. | I | High *(consumer-facing)* | **Open** — needs documentation | +| **TM-SER-02** | Under server-side prerendering the serialized state is embedded in the initial HTML response and subject to intermediary/browser caching. | I | Low | **Accepted** | +| **TM-DOM-01** | Whether `igniteui-webcomponents` / `lit-html` render bound values as text or as markup determines whether untrusted data yields DOM XSS. `lit-html` escapes interpolations by default but exposes `unsafeHTML`; usage must be confirmed for the shipped component set. To resolve: confirm with the `igniteui-webcomponents` team whether any bound value reaches `unsafeHTML`, `innerHTML` or `insertAdjacentHTML`, and record the answer plus the version it was verified against. | T | **To determine** | **Open** — must be answered before sign-off | +| **TM-DOM-02** | Consumer-supplied `RenderFragment` templates (`IgbTemplateContent`) render arbitrary consumer markup inside component-owned containers. Razor escapes `@value` by default, so this is safe unless the consumer opts into `MarkupString`. | T | Low | **By design** — documented consumer responsibility | +| — | `DynamicContentHolder.BuildRenderTree` calls `AddMarkupContent`. Microsoft's guidance explicitly names this API as an XSS vector when passed user input. **Verified**: every call site passes a static whitespace literal (`"\r\n"`, indentation) — never user data. | — | — | **Verified — no finding** | +| — | `eval` / `new Function` in first-party TypeScript. None present. | — | — | **Verified — no finding** | + +### Supply chain, build and release + +| ID | Threat | STRIDE | Sev | Status | +|---|---|---|---|---| +| **TM-SC-01** | `igniteui-webcomponents` (`~7.2.4`) and `lit-html` are bundled *inside* the .nupkg. Consumers cannot patch an upstream JS CVE independently. Upstream CVEs are handled under the `SECURITY.md` disclosure SLAs: acknowledgement within 3 business days, triage within 7 business days, fix timeline by severity. | T | Medium | **By design** — covered by the published SLAs | +| **TM-SC-02** | No SCA, CodeQL, `npm audit` or dependency-review gate. `ci.yml` runs formatting, build and tests only. | — | **High** | **Open** | +| **TM-BLD-01** | `igniteui-blazor-lite-release.yml` references `${{ env.BUILD_CONFIGURATION }}` in the signing and signature-validation steps, but that variable is **never defined**. It expands to empty, so the signing base directory becomes `src/bin/` rather than `src/bin/Release/`. It currently works only because the recursive `**/*.dll` glob still reaches the Release output — the integrity gate is scanning an unintended path. | T, R | Medium | **Open** | +| **TM-BLD-02** | Unlike the DLL step, "Validate DLL signatures" does not fail when *zero* DLLs are found — an empty result set passes the gate. | R | Medium | **Open** | +| **TM-BLD-03** | `disable` on `IgniteUI.Blazor.Lite.csproj` (generated sources are unannotated), removing compiler-enforced null safety across the shipped surface. | — | Low | **Accepted** — tracked TODO in the project file | + +## 7. Threats — `IgniteUI.Blazor.Templates` + +A template package executes no code at runtime; its risk is what it *emits* and what it +*carries*. + +| ID | Threat | STRIDE | Sev | Status | +|---|---|---|---|---| +| **TM-PKG-01** | `NoDefaultExcludes=true` combined with `Content Include="templates\**\*"` excluding only `bin`/`obj` packs **everything else in the tree** — dotfiles, `.env`, editor state, stray credentials — into the shipped package. | I | **High** | **Open** | +| **TM-PKG-02** | `test-templates.ps1` / `test-templates.sh` live in the template project; must be confirmed not to land under `content/`. | I | Medium | **Open** | +| **TM-TPL-01** | Insecure defaults in the scaffolded app (missing CSP, HSTS, HTTPS redirection, antiforgery; any CDN `