{"$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"}]}]}