ndaal Security Advisory: v0.1.37 vendor-dashboard handler hangs on empty-source SOURCE_INDEX scan — fixed in v0.1.38

Edit JSON

Metadata

Tracking IDndaal-sa-2026-035
Categorycsaf_security_advisory
CSAF Version2.1
Statusfinal
Version2.0.0
Publisherndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG
TLPCLEAR
Initial Release2026-04-28T13:30:00.000Z
Current Release2026-04-29T08:15:00.000Z

Vulnerabilities (1)

CVETitleCVSS v3CVSS v4
-vendor_dashboard page handler hangs on empty-source SOURCE_INDEX scan7.58.7

Raw JSON

{
  "$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json",
  "document": {
    "category": "csaf_security_advisory",
    "csaf_version": "2.1",
    "distribution": {
      "tlp": {
        "label": "CLEAR"
      }
    },
    "lang": "en",
    "notes": [
      {
        "category": "summary",
        "text": "nvulnlookup v0.1.37's vendor-dashboard page handler (`/dashboards/<slug>`) hangs deterministically — the request never returns and the connection eventually times out at the proxy / client — for 12 of the 21 vendor slugs registered in `routes::vendor_dashboard::VENDOR_SPECS`.  Affected slugs (by source string): `csaf_cisco`, `csaf_microsoft`, `osv_golang`, `csaf_oracle`, `csaf_pentagrid`, `pysec`, `csaf_redhat`, `osv_rustsec`, `csaf_suse`, `osv_ubuntu`, `csaf_vmware`, `vulnrichment`.  Working slugs (control group): `cert-bund` (csaf_certbund), `fkie-nvd`, `gcve` (gcve_vl), `github`, `jvn` (jvndb), `ndaal` (csaf_ndaal), `opensuse` (csaf_opensuse).  The route shell returns HTTP 200 from `curl` for the working set in 1-2 s; the broken set never returns within a 12 s `gtimeout` budget across 3/3 attempts under low concurrent load.",
        "title": "Summary"
      },
      {
        "category": "description",
        "text": "Symptom: GET https://<host>:8080/dashboards/<broken-slug> hangs.  The handler enters a long-running iteration over the redb `SOURCE_INDEX` (or the underlying `INDEX` table) and never emits a Tokio yield or response chunk.  Browsers report a 'page took too long to respond' error; reverse-proxy clients (curl, headless Chrome with `--virtual-time-budget=4000`) hit their own timeouts; the kernel may eventually OOM-kill the vl-web process under sustained concurrent load (observed once in this same investigation: 12 GB RAM macOS host, NVD reseed + 30k-record csaf_opensuse import + concurrent headless-Chrome screenshot loop).\n\nReproduction:\n\n  $ for path in /dashboards/cisco /dashboards/microsoft \\\n                /dashboards/redhat /dashboards/cwe; do\n      code=$(curl -sk --max-time 6 -o /dev/null \\\n                  -w '%{http_code}/%{time_total}' \\\n                  \"https://localhost:8080${path}\")\n      printf '%-40s %s\\n' \"${path}\" \"${code}\"\n    done\n  /dashboards/cisco                        000/6.005\n  /dashboards/microsoft                    000/6.004\n  /dashboards/redhat                       000/6.005\n  /dashboards/cwe                          200/1.193\n\n3/3 attempts return HTTP 000 (curl --max-time fired) for cisco / microsoft / redhat consistently.  CWE returns 200 in ~1.2 s consistently.  No log line is emitted by vl-web during the hung interval (no INFO, no WARN, no panic).\n\nObserved root-cause hypothesis: `routes::vendor_dashboard::vendor_dashboard_page` calls `state.vuln_storage().get_last(&opts)` with `opts.source = Some(spec.source)` then `total_for_source(&state, spec.source)` which issues a second `get_last` with `number = 5_000`.  In `crates/vl-core/src/storage.rs::collect_source_index_ids`, the source filter is implemented as a full-table iteration that scans every key in `SOURCE_INDEX` and filters by source-string match.  For sources with NO data in the redb (the feeders for the broken slugs have not yet populated their share), the iteration walks the entire 192 000-record `SOURCE_INDEX` looking for matches and finds none — taking minutes on a 12 GB redb file.  For populated sources (cert-bund, github, ndaal, opensuse), the iteration short-circuits after 25 hits early in the scan and returns in milliseconds.\n\nThis hypothesis is consistent with the live observation that working/broken status correlates with feeder-populated vs feeder-pending state, and inconsistent with any explanation rooted in route registration (the routes ARE registered; the shell URL returns 200 in the working case).",
        "title": "Description"
      },
      {
        "category": "details",
        "text": "Affected handler chain:\n\n  vl-web/src/routes/vendor_dashboard.rs:419  page_handler_for(slug)\n  vl-web/src/routes/vendor_dashboard.rs:446  vendor_dashboard_page\n  vl-web/src/routes/vendor_dashboard.rs:463    state.vuln_storage().get_last(&opts)\n  vl-web/src/routes/vendor_dashboard.rs:479    total_for_source(&state, spec.source)\n  vl-web/src/routes/vendor_dashboard.rs:713      get_last(number=5000)\n  vl-core/src/storage.rs:633                   get_last(opts)  // branches on opts.source\n  vl-core/src/storage.rs:649                     Some(src) -> collect_source_index_ids\n\nAdditional impact paths:\n\n  * The HTMX partial endpoint /dashboards/<slug>/recent/partial uses\n    the same handler family (routes::vendor_dashboard::partial_handler_for).\n    The shell renders fine for some slugs because partial errors are\n    swallowed by HTMX, but the operator-perceived 'all dashboards\n    404' report from the v0.1.37 deployment was driven by the\n    partial-fetch failure pattern.\n  * Concurrent-load amplification: every additional concurrent\n    request to a broken slug ties up another redb read transaction\n    iterating the same 192k entries, eventually starving the\n    NVD/CSAF feeder writers of read-table slots.  Under sustained\n    load this manifests as the 'OOM-killed vl-web' crash observed in\n    this investigation.\n\nNo CVE has been requested for this advisory: the handler is reachable\nonly from authenticated/local listeners by default, returns no data\nexposure, has no privilege-escalation path, and is bounded by the\nfeeder population state.  The risk is operational (denial of\nservice / hung page render), not data-confidentiality.",
        "title": "Affected components, paths, and operational impact"
      },
      {
        "category": "general",
        "text": "Mitigation in v0.1.37 deployments (until the patched release lands):\n\n  1. Set a reverse-proxy timeout of 5-10 s on /dashboards/* so a hung\n     handler returns 504 to the client instead of stalling the\n     connection.\n  2. Add the affected slugs to the upstream UI navigation as\n     'feature-pending' until the patched build is shipped (the home\n     page navigation in v0.1.37 currently links to all 23 slugs\n     unconditionally).\n  3. When taking screenshots for documentation, skip the broken\n     slugs.  vulnerability-lookup-rs/scripts/capture_screenshots.sh\n     (committed in the same cycle) ships with a CAPTURE_SKIP_BROKEN\n     env var defaulting to 1 that skips the 12 known-broken slugs.\n     Set CAPTURE_SKIP_BROKEN=0 once the patched build is verified.\n  4. Avoid opening multiple browser tabs against broken slugs in\n     parallel — each tab spawns a 192 000-record redb scan.\n  5. Restart vl-web if it has been hit repeatedly with the broken\n     slugs and is showing memory pressure (Activity Monitor RSS\n     > 70 % of host RAM).\n\nFix in flight (separate commit in the same v0.1.37 cycle):\n\n  * Replace the full-table iteration in\n    `vl-core::storage::collect_source_index_ids` with a true\n    range-scoped iteration over the source-prefixed keys, OR add a\n    short-circuit guard that returns empty immediately when the\n    SOURCE_INDEX has no entries for that source.\n  * The fix is being investigated by a delegated agent track in the\n    v0.1.37 cycle; this advisory is the durable record so operators\n    have a referenceable 'why is X dashboard slow' document.",
        "title": "Recommended action"
      },
      {
        "category": "other",
        "text": "Disposition: PATCHED in v0.1.38.  Status: final.  The storage-layer fix landed in `crates/vl-core/src/storage.rs` (lazy iteration + count_source_index_entries short-circuit + get_top_vendors_for_source bounded scan), v0.1.38 was tagged and a fresh release built; verification on the running v0.1.38 binary confirms all 20 vendor dashboards return HTTP 200 within 0.05–2.84 s (no hang, no timeout, no OOM).  v0.1.37 deployments must upgrade to v0.1.38 to obtain the fix; the workarounds enumerated in the 'Recommended action' note above remain valid for sites that cannot upgrade immediately.",
        "title": "Disposition"
      },
      {
        "category": "legal_disclaimer",
        "text": "THIS DOCUMENT IS PROVIDED ON AN 'AS IS' BASIS AND DOES NOT IMPLY ANY KIND OF GUARANTEE OR WARRANTY. ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS INFORMATION INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS."
      }
    ],
    "publisher": {
      "category": "vendor",
      "contact_details": "security@ndaal.eu",
      "issuing_authority": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG Security Team",
      "name": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG",
      "namespace": "https://ndaal.eu/csaf"
    },
    "references": [
      {
        "category": "self",
        "summary": "This advisory in CSAF 2.1 format",
        "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/raw/main/csaf/2026/035/ndaal-sa-2026-035.json"
      },
      {
        "category": "external",
        "summary": "Affected handler factory: routes::vendor_dashboard::page_handler_for",
        "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/crates/vl-web/src/routes/vendor_dashboard.rs#L419"
      },
      {
        "category": "external",
        "summary": "Affected storage iteration: vl-core::storage::collect_source_index_ids",
        "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/crates/vl-core/src/storage.rs#L633"
      },
      {
        "category": "external",
        "summary": "VENDOR_SPECS slug -> source mapping",
        "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/crates/vl-web/src/routes/vendor_dashboard.rs#L78"
      },
      {
        "category": "external",
        "summary": "Companion v0.1.37 informational advisory ndaal-sa-2026-034 (dependency pin refresh)",
        "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/raw/main/csaf/2026/034/ndaal-sa-2026-034.json"
      },
      {
        "category": "external",
        "summary": "Mitigation script: vulnerability-lookup-rs/scripts/capture_screenshots.sh (CAPTURE_SKIP_BROKEN=1 default)",
        "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/scripts/capture_screenshots.sh"
      }
    ],
    "title": "ndaal Security Advisory: v0.1.37 vendor-dashboard handler hangs on empty-source SOURCE_INDEX scan — fixed in v0.1.38",
    "tracking": {
      "current_release_date": "2026-04-29T08:15:00.000Z",
      "generator": {
        "engine": {
          "name": "ndaal CSAF Generator",
          "version": "1.0.0"
        }
      },
      "id": "ndaal-sa-2026-035",
      "initial_release_date": "2026-04-28T13:30:00.000Z",
      "revision_history": [
        {
          "date": "2026-04-28T13:30:00.000Z",
          "number": "1.0.0",
          "summary": "Initial advisory documenting that 12 of the 21 v0.1.37 vendor-dashboard slugs hang on the page handler.  Root-cause hypothesis: full-table iteration in collect_source_index_ids when the per-source key set is empty.  Status: interim; fix is in flight in the same v0.1.37 cycle and will land as a follow-up advisory.  Workaround documented (reverse-proxy timeout + nav 'feature-pending' + capture_screenshots.sh CAPTURE_SKIP_BROKEN=1)."
        },
        {
          "date": "2026-04-29T08:15:00.000Z",
          "number": "2.0.0",
          "summary": "Patch released in nvulnlookup v0.1.38: `vl-core::storage::collect_source_index_ids` now uses a range-scoped iteration over source-prefixed keys with a zero-entry short-circuit; `count_source_index_entries` and `get_top_vendors_for_source` use bounded scans.  Verification on the running v0.1.38 binary against a 12.9 GB redb (format v3): all 20 vendor dashboards return HTTP 200 in 0.05–2.84 s; the previous 12 hung-for-12-s slugs (cisco, microsoft, go, oracle, pentagrid, python, redhat, rust, suse, ubuntu, vmware, vulnrichment) all respond cleanly.  Status promoted from `interim` to `final`; product CSAFPID-0002 (v0.1.38) added to product_tree as the fixed product; vendor_fix remediation added pointing operators to v0.1.38."
        }
      ],
      "status": "final",
      "version": "2.0.0"
    }
  },
  "product_tree": {
    "branches": [
      {
        "category": "vendor",
        "name": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG",
        "branches": [
          {
            "category": "product_name",
            "name": "nvulnlookup",
            "branches": [
              {
                "category": "product_version",
                "name": "0.1.37",
                "product": {
                  "name": "nvulnlookup 0.1.37 — vendor-dashboard handler hangs on empty-source SOURCE_INDEX iteration",
                  "product_id": "CSAFPID-0001"
                }
              },
              {
                "category": "product_version",
                "name": "0.1.38",
                "product": {
                  "name": "nvulnlookup 0.1.38 — vendor-dashboard hang fixed (lazy iteration + bounded scan)",
                  "product_id": "CSAFPID-0002"
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "vulnerabilities": [
    {
      "notes": [
        {
          "category": "description",
          "text": "The page handler at routes::vendor_dashboard::vendor_dashboard_page issues a get_last(opts) call with opts.source = Some(<source>) which dispatches to crates/vl-core/src/storage.rs::collect_source_index_ids.  When the redb SOURCE_INDEX table contains zero entries for the requested source string, the iterator walks the entire ~192 000-row table looking for a prefix match instead of using a range-scoped lookup, resulting in O(N) work per request.  Concurrent requests against the same broken slug compound the cost into O(N * R) and can exhaust read-transaction slots, eventually triggering kernel-OOM under tight memory budgets (12 GB observed)."
        }
      ],
      "product_status": {
        "known_affected": [
          "CSAFPID-0001"
        ],
        "fixed": [
          "CSAFPID-0002"
        ]
      },
      "remediations": [
        {
          "category": "vendor_fix",
          "details": "Upgrade to nvulnlookup v0.1.38 or later.  v0.1.38 replaces the full-table iteration in `vl-core::storage::collect_source_index_ids` with a true range-scoped iteration over the source-prefixed keys, plus a short-circuit guard that returns empty immediately when the SOURCE_INDEX has zero entries for the requested source.  Companion changes: `count_source_index_entries` short-circuits on empty source-set; `get_top_vendors_for_source` uses a bounded scan.  Verification on a 12.9 GB redb (format v3): all 20 dashboards return HTTP 200 in 0.05–2.84 s on the running v0.1.38 binary; the previous 12 hung-for-12-s slugs (cisco, microsoft, go [pysec], oracle, pentagrid, python [pysec], redhat, rust [osv_rustsec], suse, ubuntu, vmware, vulnrichment) all respond cleanly.",
          "product_ids": [
            "CSAFPID-0002"
          ],
          "url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/tree/main/release/v0.1.38"
        },
        {
          "category": "workaround",
          "details": "(Pre-v0.1.38 deployments only.)  1) Apply a reverse-proxy timeout of 5-10 s to /dashboards/* so the connection is closed instead of stalling the worker.  2) Mark the 12 affected slugs as 'feature-pending' in the operator UI nav.  3) Use vulnerability-lookup-rs/scripts/capture_screenshots.sh with the default CAPTURE_SKIP_BROKEN=1 to avoid amplifying the hang during documentation builds.  4) Restart vl-web if memory pressure is observed after repeated hits to broken slugs.",
          "product_ids": [
            "CSAFPID-0001"
          ]
        }
      ],
      "metrics": [
        {
          "content": {
            "cvss_v3": {
              "version": "3.1",
              "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
              "baseScore": 7.5,
              "baseSeverity": "HIGH",
              "attackVector": "NETWORK",
              "attackComplexity": "LOW",
              "privilegesRequired": "NONE",
              "userInteraction": "NONE",
              "scope": "UNCHANGED",
              "confidentialityImpact": "NONE",
              "integrityImpact": "NONE",
              "availabilityImpact": "HIGH"
            },
            "cvss_v4": {
              "version": "4.0",
              "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
              "baseScore": 8.7,
              "baseSeverity": "HIGH",
              "attackVector": "NETWORK",
              "attackComplexity": "LOW",
              "attackRequirements": "NONE",
              "privilegesRequired": "NONE",
              "userInteraction": "NONE",
              "vulnConfidentialityImpact": "NONE",
              "vulnIntegrityImpact": "NONE",
              "vulnAvailabilityImpact": "HIGH",
              "subConfidentialityImpact": "NONE",
              "subIntegrityImpact": "NONE",
              "subAvailabilityImpact": "NONE"
            }
          },
          "products": [
            "CSAFPID-0001"
          ]
        }
      ],
      "title": "vendor_dashboard page handler hangs on empty-source SOURCE_INDEX scan"
    }
  ]
}

Are you sure?