ndaal Security Advisory: Database restore accepted unverified dumps and replayed SQL with unrestricted engine authority (grundschutz-oscal-viewer < 1.6.54)

Edit JSON

Metadata

Tracking IDndaal-sa-2026-416
Categorycsaf_security_advisory
CSAF Version2.1
Statusfinal
Version1.0.0
Publisherndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG
TLPCLEAR
Initial Release2026-08-13T12:00:00.000Z
Current Release2026-08-13T12:00:00.000Z

Vulnerabilities (1)

CVETitleCVSS v3CVSS v4
-Database restore accepted unverified dumps and replayed SQL with unrestricted engine authority6.66.8

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": "`--restore-dump` rebuilt an annotation database from a dump file without verifying any of the five checksum sidecars that the viewer's own dump writer emits beside it, and replayed `.sql` dumps through `execute_batch` with the SQLite engine's full authority — no ATTACH restriction and no validation of the schema the replay produced.",
        "title": "Summary"
      },
      {
        "category": "details",
        "text": "1.6.54 fixes both halves. Integrity: restore_dump now calls the new `sidecar::verify_sidecars`, which re-derives every digest whose sidecar exists and refuses the restore on any mismatch; a source with NO sidecar beside it is refused as well, because an attacker who can rewrite a dump can equally delete its sidecars, so verifying only what happens to be present would verify nothing. Replay authority: restore_sql_dump sets SQLITE_LIMIT_ATTACHED = 0 (via the rusqlite `limits` feature) so ATTACH fails inside the engine — a limit, not a text filter, so quoting and comment tricks cannot evade it — and a new post-replay check refuses any TRIGGER or VIEW the dump left behind (the executable schema: a trigger fires on every later annotation write, a view re-points a name the application reads) and requires the four annotation tables to be present. Additional bare tables are deliberately permitted — they execute nothing and the application never reads a table it does not know, so refusing them would break legitimate dumps without closing any execution path. Both are pinned by regression tests (`restore_dump_refuses_a_tampered_or_unverifiable_dump`, `restore_sql_dump_refuses_attach_and_rogue_schema`) and by a new coverage-guided fuzz target, `fuzz_verify_sidecars`. That target earned its place on its first run: it found that a sidecar which existed but was not readable UTF-8 (a single 0xFF byte) was silently skipped as though absent, contradicting the verifier's own documented error contract. Present-but-unreadable is now a refusal. The restore path was fail-closed either way — an unreadable sidecar left the verified count at zero, which the caller already refuses — so this tightened the contract rather than closing a second hole.",
        "title": "Remediation"
      },
      {
        "category": "general",
        "text": "Upgrade to grundschutz-oscal-viewer 1.6.54. Operator-visible behaviour change: a dump with no checksum sidecar beside it is now REFUSED. Restoring a hand-made or third-party dump requires generating a sidecar first, for example `shasum -a 256 dump.sql > dump.sql.sha-256`. Dumps produced by the viewer already carry all five and restore unchanged. Keep dump directories on storage only the operator can write to; the checksum verification detects tampering but does not prevent it.",
        "title": "Recommended Action"
      },
      {
        "category": "general",
        "text": "Discovered by ndaal's own OWASP ASVS v5.0.0 audit of grundschutz-oscal-viewer on 2026-08-13, which produced the product's first STRIDE threat model; this defect is recorded there as S-T3/S-E1. No external report, CVE or RUSTSEC advisory exists for it.",
        "title": "Discovery"
      },
      {
        "category": "general",
        "text": "grundschutz-oscal-viewer is a secure, memory-safe, single-binary offline viewer for the BSI Grundschutz++ OSCAL catalog — embedded catalog data, filters and full-text search served over a TLS 1.3 (rustls) HTTP/1.1 + HTTP/2 listener, with an annotations workspace offering optional XChaCha20-Poly1305 encryption-at-rest and embedded SQLite storage.",
        "title": "Product Description"
      },
      {
        "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.",
        "title": "Legal Disclaimer"
      }
    ],
    "publisher": {
      "category": "vendor",
      "contact_details": "security@ndaal.eu",
      "issuing_authority": "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG Security Team",
      "name": "ndaal Gesellschaft für 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_csaf_information/-/raw/main/csaf/2026/416/ndaal-sa-2026-416.json"
      },
      {
        "category": "external",
        "summary": "grundschutz-oscal-viewer source code",
        "url": "https://gitlab.com/vPierre/ndaal_public_bsi_grundschutz_oscal_viewer"
      },
      {
        "category": "external",
        "summary": "grundschutz-oscal-viewer on crates.io",
        "url": "https://crates.io/crates/grundschutz-oscal-viewer"
      }
    ],
    "title": "ndaal Security Advisory: Database restore accepted unverified dumps and replayed SQL with unrestricted engine authority (grundschutz-oscal-viewer < 1.6.54)",
    "tracking": {
      "current_release_date": "2026-08-13T12:00:00.000Z",
      "generator": {
        "engine": {
          "name": "ndaal CSAF Generator",
          "version": "1.0.0"
        }
      },
      "id": "ndaal-sa-2026-416",
      "initial_release_date": "2026-08-13T12:00:00.000Z",
      "revision_history": [
        {
          "date": "2026-08-13T12:00:00.000Z",
          "number": "1.0.0",
          "summary": "Initial advisory: S-T3/S-E1 remediated in grundschutz-oscal-viewer 1.6.54."
        }
      ],
      "status": "final",
      "version": "1.0.0"
    }
  },
  "product_tree": {
    "branches": [
      {
        "category": "vendor",
        "name": "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG",
        "branches": [
          {
            "category": "product_name",
            "name": "grundschutz-oscal-viewer",
            "branches": [
              {
                "category": "product_version",
                "name": "1.6.53",
                "product": {
                  "name": "ndaal Grundschutz++ OSCAL Viewer 1.6.53",
                  "product_id": "CSAFPID-0001"
                }
              },
              {
                "category": "product_version",
                "name": "1.6.54",
                "product": {
                  "name": "ndaal Grundschutz++ OSCAL Viewer 1.6.54",
                  "product_id": "CSAFPID-0002"
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "vulnerabilities": [
    {
      "ids": [
        {
          "system_name": "ndaal Grundschutz++ OSCAL Viewer threat model",
          "text": "S-T3/S-E1"
        }
      ],
      "notes": [
        {
          "category": "description",
          "text": "Two defects on the same operator-initiated path. (1) Integrity: run_dump writes .sha-256/.sha-512/.sha3-512/.blake3-512/.shake256-512 sidecars for every dump, but restore_dump (src/dbdump.rs) validated only the SQLite magic, UTF-8 encoding and size caps — never the digests. Anyone able to write to the dump directory could therefore alter a dump, and a later restore would present the rewritten annotation history as authentic, defeating the append-only versioning the product relies on for its audit trail. (2) Replay authority: Store::restore_sql_dump (src/store.rs) executed the dump text as a single batch, so a hostile `.sql` could ATTACH a second database — reading or overwriting any file the viewer process could reach — and could leave triggers or views behind that fire on every subsequent annotation write, persisting long after the restore completed. Exploitation requires write access to a dump file the operator later chooses to restore, and the operator action of running --restore-dump, which is why the vector is AV:L/PR:L with user interaction required. Impact when those preconditions are met is high for both confidentiality and integrity. Rated CWE-345 with CWE-501 (trust-boundary violation) as the secondary weakness; ASVS V1.3 (safe deserialization of untrusted input) and V13.4.",
          "title": "Description"
        },
        {
          "category": "details",
          "text": "1.6.54 fixes both halves. Integrity: restore_dump now calls the new `sidecar::verify_sidecars`, which re-derives every digest whose sidecar exists and refuses the restore on any mismatch; a source with NO sidecar beside it is refused as well, because an attacker who can rewrite a dump can equally delete its sidecars, so verifying only what happens to be present would verify nothing. Replay authority: restore_sql_dump sets SQLITE_LIMIT_ATTACHED = 0 (via the rusqlite `limits` feature) so ATTACH fails inside the engine — a limit, not a text filter, so quoting and comment tricks cannot evade it — and a new post-replay check refuses any TRIGGER or VIEW the dump left behind (the executable schema: a trigger fires on every later annotation write, a view re-points a name the application reads) and requires the four annotation tables to be present. Additional bare tables are deliberately permitted — they execute nothing and the application never reads a table it does not know, so refusing them would break legitimate dumps without closing any execution path. Both are pinned by regression tests (`restore_dump_refuses_a_tampered_or_unverifiable_dump`, `restore_sql_dump_refuses_attach_and_rogue_schema`) and by a new coverage-guided fuzz target, `fuzz_verify_sidecars`. That target earned its place on its first run: it found that a sidecar which existed but was not readable UTF-8 (a single 0xFF byte) was silently skipped as though absent, contradicting the verifier's own documented error contract. Present-but-unreadable is now a refusal. The restore path was fail-closed either way — an unreadable sidecar left the verified count at zero, which the caller already refuses — so this tightened the contract rather than closing a second hole. CVSS v3.1 base score 6.6 (MEDIUM), vector CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N. CVSS v4.0 base score 6.8 (MEDIUM), vector CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N.",
          "title": "Remediation Detail"
        }
      ],
      "product_status": {
        "known_affected": [
          "CSAFPID-0001"
        ],
        "fixed": [
          "CSAFPID-0002"
        ]
      },
      "remediations": [
        {
          "category": "vendor_fix",
          "details": "Upgrade to grundschutz-oscal-viewer 1.6.54. Operator-visible behaviour change: a dump with no checksum sidecar beside it is now REFUSED. Restoring a hand-made or third-party dump requires generating a sidecar first, for example `shasum -a 256 dump.sql > dump.sql.sha-256`. Dumps produced by the viewer already carry all five and restore unchanged. Keep dump directories on storage only the operator can write to; the checksum verification detects tampering but does not prevent it.",
          "product_ids": [
            "CSAFPID-0002"
          ]
        }
      ],
      "metrics": [
        {
          "content": {
            "cvss_v3": {
              "version": "3.1",
              "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N",
              "baseScore": 6.6,
              "baseSeverity": "MEDIUM",
              "attackVector": "LOCAL",
              "attackComplexity": "LOW",
              "privilegesRequired": "LOW",
              "userInteraction": "REQUIRED",
              "scope": "UNCHANGED",
              "confidentialityImpact": "HIGH",
              "integrityImpact": "HIGH",
              "availabilityImpact": "NONE"
            },
            "cvss_v4": {
              "version": "4.0",
              "vectorString": "CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
              "baseScore": 6.8,
              "baseSeverity": "MEDIUM",
              "attackVector": "LOCAL",
              "attackComplexity": "LOW",
              "attackRequirements": "NONE",
              "privilegesRequired": "LOW",
              "userInteraction": "ACTIVE",
              "vulnConfidentialityImpact": "HIGH",
              "vulnIntegrityImpact": "HIGH",
              "vulnAvailabilityImpact": "NONE",
              "subConfidentialityImpact": "NONE",
              "subIntegrityImpact": "NONE",
              "subAvailabilityImpact": "NONE"
            }
          },
          "products": [
            "CSAFPID-0001"
          ]
        }
      ],
      "threats": [
        {
          "category": "impact",
          "details": "Two defects on the same operator-initiated path. (1) Integrity: run_dump writes .sha-256/.sha-512/.sha3-512/.blake3-512/.shake256-512 sidecars for every dump, but restore_dump (src/dbdump.rs) validated only the SQLite magic, UTF-8 encoding and size caps — never the digests. Anyone able to write to the dump directory could therefore alter a dump, and a later restore would present the rewritten annotation history as authentic, defeating the append-only versioning the product relies on for its audit trail. (2) Replay authority: Store::restore_sql_dump (src/store.rs) executed the dump text as a single batch, so a hostile `.sql` could ATTACH a second database — reading or overwriting any file the viewer process could reach — and could leave triggers or views behind that fire on every subsequent annotation write, persisting long after the restore completed. Exploitation requires write access to a dump file the operator later chooses to restore, and the operator action of running --restore-dump, which is why the vector is AV:L/PR:L with user interaction required. Impact when those preconditions are met is high for both confidentiality and integrity. Rated CWE-345 with CWE-501 (trust-boundary violation) as the secondary weakness; ASVS V1.3 (safe deserialization of untrusted input) and V13.4.",
          "product_ids": [
            "CSAFPID-0001"
          ]
        }
      ],
      "title": "Database restore accepted unverified dumps and replayed SQL with unrestricted engine authority",
      "references": [
        {
          "category": "external",
          "summary": "Threat model entry",
          "url": "https://gitlab.com/vPierre/ndaal_public_bsi_grundschutz_oscal_viewer/-/blob/main/documentation/threat-model/stride.md"
        }
      ]
    }
  ]
}

Are you sure?