bug(adminui): /scripts is a hard 500 when any Script.SourceHash is shorter than 12 chars #504

Closed
opened 2026-07-26 09:06:58 -04:00 by dohertj2 · 2 comments
Owner

Found while live-gating #487 on the docker-dev rig.

Components/Pages/Scripts.razor:41 renders the hash prefix as:

<span class="text-muted small ms-2 mono">hash=@s.SourceHash[..12]…</span>

SourceHash is a plain nvarchar(64) column with no length floor. Any row whose hash is shorter than 12 characters throws ArgumentOutOfRangeException out of BuildRenderTree, which is unhandled and takes the whole /scripts page to a hard HTTP 500 — not just that row.

Reproduction

The docker-dev seed itself trips it. seed/seed-clusters.sql inserts placeholder hashes:

ScriptId SourceHash length
SC-gatevt h-abs-hr200 11
SC-cval h1 2

So on a stock docker-dev bring-up, GET /scripts is a 500 with:

System.ArgumentOutOfRangeException: Index and length must refer to a location within the string. (Parameter 'length')
   at System.String.Substring(Int32 startIndex, Int32 length)
   at ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Scripts.BuildRenderTree(RenderTreeBuilder __builder) in Scripts.razor:line 41

Severity

Lower than it first looks: scripts authored through ScriptEdit.razor / UnsTreeService.HashSource always get a full 64-char SHA-256, so a fleet that only ever authored through the UI will not hit this. It matters because (a) the seeded dev rig hits it every time, and (b) a range operator applied to an unvalidated DB string is a latent whole-page crash — nothing enforces the length at the schema, entity, or render layer.

Suggested fix

Truncate defensively rather than slice, e.g. @(s.SourceHash.Length > 12 ? s.SourceHash[..12] + "…" : s.SourceHash). Worth a sweep for other [..n] range operators applied to DB-sourced strings in the AdminUI while in there. Optionally also fix the seed to insert real SHA-256 values so the dev rig stops shipping a broken page.

Not related to #487 — filing separately so it is tracked rather than lost in that PR.

Found while live-gating #487 on the docker-dev rig. `Components/Pages/Scripts.razor:41` renders the hash prefix as: ```razor <span class="text-muted small ms-2 mono">hash=@s.SourceHash[..12]…</span> ``` `SourceHash` is a plain `nvarchar(64)` column with no length floor. Any row whose hash is shorter than 12 characters throws `ArgumentOutOfRangeException` out of `BuildRenderTree`, which is unhandled and takes the **whole `/scripts` page** to a hard HTTP 500 — not just that row. ## Reproduction The docker-dev seed itself trips it. `seed/seed-clusters.sql` inserts placeholder hashes: | ScriptId | SourceHash | length | |---|---|---| | `SC-gatevt` | `h-abs-hr200` | 11 | | `SC-cval` | `h1` | 2 | So on a stock docker-dev bring-up, `GET /scripts` is a 500 with: ``` System.ArgumentOutOfRangeException: Index and length must refer to a location within the string. (Parameter 'length') at System.String.Substring(Int32 startIndex, Int32 length) at ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Scripts.BuildRenderTree(RenderTreeBuilder __builder) in Scripts.razor:line 41 ``` ## Severity Lower than it first looks: scripts authored through `ScriptEdit.razor` / `UnsTreeService.HashSource` always get a full 64-char SHA-256, so a fleet that only ever authored through the UI will not hit this. It matters because (a) the seeded dev rig hits it every time, and (b) a range operator applied to an unvalidated DB string is a latent whole-page crash — nothing enforces the length at the schema, entity, or render layer. ## Suggested fix Truncate defensively rather than slice, e.g. `@(s.SourceHash.Length > 12 ? s.SourceHash[..12] + "…" : s.SourceHash)`. Worth a sweep for other `[..n]` range operators applied to DB-sourced strings in the AdminUI while in there. Optionally also fix the seed to insert real SHA-256 values so the dev rig stops shipping a broken page. Not related to #487 — filing separately so it is tracked rather than lost in that PR.
Author
Owner

Correction to the reproduction section below — the docker-dev seed is not the source. I inferred that from the rig's data without checking seed/seed-clusters.sql; it only inserts ServerCluster, ClusterNode, and LdapGroupRoleMapping. The two short-hash rows on the rig (SC-gatevt = h-abs-hr200, SC-cval = h1) came from earlier live-gate authoring in past sessions, not from the seed. So a freshly seeded docker-dev does not have a broken /scripts page — only a rig that has accumulated API- or SQL-authored Script rows does.

That narrows the trigger but does not remove it: nothing enforces a minimum SourceHash length at the schema, entity, or render layer, so any row that did not go through ScriptEdit.razor / UnsTreeService.HashSource — REST-API authored, hand-inserted, migrated in from another system — still hard-500s the whole page.

Fixed

New Components/Shared/DisplayText.Abbreviate(string?, int) — null/blank-safe, never throws, and appends the ellipsis only when it actually truncated (so a 2-character hash renders h1, not h1…, and no longer implies text that is not there). A maxLength < 1 is clamped rather than allowed to throw.

Applied to every unguarded range-operator slice over a DB-sourced string found by sweeping the AdminUI for [..N]:

file value
Components/Pages/Scripts.razor:41 SourceHash — the reported crash
Components/Pages/Certificates.razor:84 Thumbprint (read off the on-disk cert store)
Components/Pages/Deployments.razor:58 RevisionHash
Components/Pages/Deployments.razor:116 RevisionHash in the dispatch banner
Components/Pages/Clusters/ClusterOverview.razor:61 RevisionHash

The rest of the [..N] hits in the sweep are safe and were left alone: Guid.ToString("N") slices (always 32 chars) and the Length > 60 ? [..60] : … ternaries that already guard themselves.

**Correction to the reproduction section below — the docker-dev *seed* is not the source.** I inferred that from the rig's data without checking `seed/seed-clusters.sql`; it only inserts `ServerCluster`, `ClusterNode`, and `LdapGroupRoleMapping`. The two short-hash rows on the rig (`SC-gatevt` = `h-abs-hr200`, `SC-cval` = `h1`) came from earlier live-gate authoring in past sessions, not from the seed. So a **freshly seeded** docker-dev does *not* have a broken `/scripts` page — only a rig that has accumulated API- or SQL-authored Script rows does. That narrows the trigger but does not remove it: nothing enforces a minimum `SourceHash` length at the schema, entity, or render layer, so any row that did not go through `ScriptEdit.razor` / `UnsTreeService.HashSource` — REST-API authored, hand-inserted, migrated in from another system — still hard-500s the whole page. ## Fixed New `Components/Shared/DisplayText.Abbreviate(string?, int)` — null/blank-safe, never throws, and appends the ellipsis **only when it actually truncated** (so a 2-character hash renders `h1`, not `h1…`, and no longer implies text that is not there). A `maxLength < 1` is clamped rather than allowed to throw. Applied to every unguarded range-operator slice over a DB-sourced string found by sweeping the AdminUI for `[..N]`: | file | value | |---|---| | `Components/Pages/Scripts.razor:41` | `SourceHash` — the reported crash | | `Components/Pages/Certificates.razor:84` | `Thumbprint` (read off the on-disk cert store) | | `Components/Pages/Deployments.razor:58` | `RevisionHash` | | `Components/Pages/Deployments.razor:116` | `RevisionHash` in the dispatch banner | | `Components/Pages/Clusters/ClusterOverview.razor:61` | `RevisionHash` | The rest of the `[..N]` hits in the sweep are safe and were left alone: `Guid.ToString("N")` slices (always 32 chars) and the `Length > 60 ? [..60] : …` ternaries that already guard themselves.
Author
Owner

Merged + pushed — origin/master e3155fbb (from fix/504-abbreviate-db-strings / 47d148da).

Merged tree builds clean (0 errors); AdminUI suite 739/739 (14 of them new in DisplayTextTests, covering short / null / blank / clamped budgets plus a never-throws sweep over every value x budget combination).

Live-verified on docker-dev (AdminUI has no bUnit, so this is the real check), same rig and same two short-hash rows, pre-fix image vs. rebuilt:

pre-fix post-fix
GET /scripts 500 200, on both central nodes
rendering page dead hash=h1 and hash=h-abs-hr200, full, no misleading ellipsis
/deployments unchanged: 64-char hashes still truncate at 12 with the ellipsis
/certificates, /clusters/MAIN 200, unchanged
Merged + pushed — `origin/master` **`e3155fbb`** (from `fix/504-abbreviate-db-strings` / `47d148da`). Merged tree builds clean (0 errors); AdminUI suite **739/739** (14 of them new in `DisplayTextTests`, covering short / null / blank / clamped budgets plus a never-throws sweep over every value x budget combination). **Live-verified on docker-dev** (AdminUI has no bUnit, so this is the real check), same rig and same two short-hash rows, pre-fix image vs. rebuilt: | | pre-fix | post-fix | |---|---|---| | `GET /scripts` | **500** | **200**, on both central nodes | | rendering | page dead | `hash=h1` and `hash=h-abs-hr200`, full, no misleading ellipsis | | `/deployments` | — | unchanged: 64-char hashes still truncate at 12 with the ellipsis | | `/certificates`, `/clusters/MAIN` | — | 200, unchanged |
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#504