Security Review — 2026-03-01

Changelog

Date Changes

2026-03-01 (initial)

Initial full security review at commit 992d7c1. 10 findings (SEC-001 to SEC-010) identified.

2026-03-01 (fixes)

Fixed SEC-002 (predictable temp file), SEC-003 (data race), SEC-009 (Go 1.23.0 → 1.24.0), SEC-010 (x/sys v0.13.0 → v0.41.0). Merged as PR #77.

2026-03-01 (tooling)

Added Makefile with gosec, staticcheck, nilaway, govulncheck, go vet (PR #78). First automated scan identified 2 new findings: SEC-011 (unhandled error), SEC-012 (potential nil panics). Updated dependency status table.

2026-03-03 (sub-cells)

Updated SEC-008 assessment: ParseLabel/stripTags is now a backward-compatible fallback only, reducing its attack surface. New sub-cell architecture (#232) uses plain-text value attributes on child <mxCell> elements, avoiding HTML parsing for new elements.

2026-03-05 (nilaway + review)

Fixed SEC-012 (nil panics from GetPage()) via requirePage test helpers and strings.Cut refactor (#235). Full code review + security review of metadata feature, nilaway fixes, Kroki config, Dockerfile. Added SEC-013 (sanitizeID permissiveness) and SEC-014 (Dockerfile script downloads without checksums). HTML escaping in new metadata.go confirmed correct.

2026-03-30 (full review)

Full security review at current main. Verified fixes: SEC-004 (now 0600), SEC-007 (MaxElementDepth=50), SEC-011 (_ = tmp.Close()). Added SEC-015 (view key path traversal in export filenames), SEC-016 (no output-dir boundary check), SEC-017 (govulncheck broken). All dependencies current, no new CVEs. gosec/staticcheck/nilaway clean. Re-confirmed XXE not exploitable.

2026-04-27 (typo-squatting)

Fixed SEC-018: Go module path, go install URL in README, and landing page buttons all used misspelled Bauteinsicht (missing "s") or wrong GitHub owner (rdmueller). Renamed module to github.com/docToolchain/Bausteinsicht, corrected all URLs. Also fixed dead link to CLAUDE.md in trust model (../../../../../).

2026-04-27 (govulncheck)

Fixed SEC-017: govulncheck v1.1.4 (Go 1.22.2) could not analyze fsnotify v1.9.0 internal packages. Updated to v1.3.0 (Go 1.25.9) — scan now passes with 0 vulnerabilities in called code.

2026-04-27 (path traversal)

Fixed SEC-015: view keys used directly in export filenames without sanitization. Added SafeViewKey() using filepath.Base to strip directory components at all three export paths.

2026-04-27 (batch fix)

Fixed SEC-001 (path traversal via CLI flags — reject .. in --model/--template/--output), SEC-005 (error on multiple .jsonc files), SEC-006 (10 MB file size limit), SEC-008 (stripTags attribute quoting), SEC-013 (sanitizeID strips dots/slashes), SEC-016 (--output path traversal). SEC-014 (Dockerfile checksums) deferred — upstream projects don’t publish checksums.

Scope

Full security review of the Bausteinsicht repository at commit 992d7c1 (main branch, 2026-03-01).

Reviewed areas:

  • All Go source files in cmd/bausteinsicht/ (CLI layer)

  • All Go source files in internal/ (drawio, model, sync, watcher packages)

  • All direct and transitive dependencies (go.mod / go.sum)

  • Dependency vulnerability scan via govulncheck and OSV database

Threat Model

Bausteinsicht is a local CLI tool that reads/writes files on the user’s workstation. The trust boundary is:

  • Trusted input: CLI flags provided by the operator

  • Semi-trusted input: JSONC model files and draw.io XML files (may be shared via Git between team members)

  • No network exposure: No HTTP server, no outbound API calls, no TLS server

The primary risk scenario is a malicious or crafted model/drawio file shared via a repository that causes unexpected behavior when another user runs bausteinsicht sync.

Summary

ID Severity Issue Status

SEC-001

Medium

No path boundary check on --model / --template flags; write paths derived from user-controlled input

Fixed (reject .. traversal)

SEC-002

Low-Medium

Predictable temp file name in model.Save (TOCTOU / symlink attack)

Fixed (PR #77)

SEC-003

Low-Medium

Data race on lastFile variable in watcher debounce closure

Fixed (PR #77)

SEC-004

Low

File permissions hardcoded to 0644 (world-readable) for model and template files

Fixed (init.go now uses 0600)

SEC-005

Low

AutoDetect silently picks first .jsonc file alphabetically (confused deputy)

Fixed (error on multiple files)

SEC-006

Low

No file size limit on JSONC model parsing (memory exhaustion DoS)

Fixed (10 MB limit)

SEC-007

Low

No recursion depth limit on Element.Children traversal (stack overflow DoS)

Fixed (MaxElementDepth=50)

SEC-008

Low

stripTags incorrectly handles > inside HTML attribute values

Fixed (attribute quoting)

SEC-009

Medium

Go toolchain 1.23.0 has 25 known stdlib vulnerabilities

Fixed (PR #77, upgraded to 1.24.0)

SEC-010

Low

golang.org/x/sys v0.13.0 is 28 minor versions behind current

Fixed (PR #77, upgraded to v0.41.0)

SEC-011

Low

Unhandled tmp.Close() error in model.Save cleanup path

Fixed (_ = tmp.Close())

SEC-015

Medium

View key used directly in export filenames without path traversal sanitization

Fixed (SafeViewKey)

SEC-016

Low

--output directory in export commands has no boundary check (absolute paths allowed)

Fixed (reject .. traversal)

SEC-017

Info

govulncheck fails with internal error on golang.org/x/sys/unix import from fsnotify

Fixed (updated to v1.3.0)

SEC-012

Low

Potential nil panics from unchecked GetPage() return values

Fixed (#235)

SEC-013

Low

sanitizeID in diff.go does not strip dots/slashes from titles, potentially creating IDs that interfere with dot-notation hierarchy

Fixed (strips dots/slashes)

SEC-014

Medium

Dockerfile downloads and pipes install scripts (golangci-lint, Claude Code, human CLI) without checksum verification

Deferred (upstream projects don’t publish checksums)

SEC-018

Medium

Typo-squatting risk: Go module path, README go install, and landing page all used misspelled Bauteinsicht or wrong owner rdmueller

Fixed

Findings

SEC-001: Path Traversal via CLI Flags (Medium)

Affected files:

  • cmd/bausteinsicht/sync.go:27-28

  • cmd/bausteinsicht/watch.go:29-30

  • cmd/bausteinsicht/validate.go:34

  • cmd/bausteinsicht/add_element.go:40

  • cmd/bausteinsicht/add_relationship.go:34

  • internal/model/loader.go:13-14

  • internal/drawio/template.go:27-33

Description: The --model and --template CLI flags accept arbitrary filesystem paths without validation. No call to filepath.Clean or boundary check exists. More critically, write targets are derived from the model path:

dir := filepath.Dir(modelPath)          // attacker-controlled
drawioPath := filepath.Join(dir, "architecture.drawio")
statePath  := filepath.Join(dir, ".bausteinsicht-sync")

Supplying --model /tmp/../../some/path/x.jsonc causes writes at attacker-chosen locations.

Risk: For a local CLI tool where the operator provides their own flags, this is acceptable. If the CLI is ever wrapped in automation (CI/CD, LLM agent pipelines), this becomes exploitable.

Tool correlation: Also flagged by gosec as G304 (CWE-22) in loader.go:14, template.go:28, state.go:44, state.go:100.

Recommendation: After resolving the user-supplied path with filepath.Abs, verify the result is rooted at an allowed base directory. At minimum, document that paths are fully trusted and the tool must not be run with untrusted flag values.

SEC-002: Predictable Temp File in model.Save (Low-Medium) — FIXED

Affected file: internal/model/loader.go:32-38

Fix applied in PR #77: Replaced path + ".tmp" with os.CreateTemp(dir, ".model-tmp-*"), aligning with the safe pattern used in SaveDocument and SaveState.

SEC-003: Data Race in Watcher Debounce (Low-Medium) — FIXED

Affected file: internal/watcher/watcher.go:99-108

Fix applied in PR #77: Added captured := lastFile before time.AfterFunc closure to capture the variable by value instead of by reference. Verified with go test -race ./…​.

SEC-004: World-Readable File Permissions (Low) — FIXED

Fix verified 2026-03-30: init.go now uses 0600 for both model and template files. model.Save uses atomic temp file with restricted permissions.

SEC-005: AutoDetect Confused Deputy (Low)

Affected file: internal/model/loader.go:56-65

func AutoDetect(dir string) (string, error) {
    matches, err := filepath.Glob(filepath.Join(dir, "*.jsonc"))
    ...
    return matches[0], nil   // first alphabetical match
}

Description: When multiple .jsonc files exist, the tool silently picks the first alphabetically. A malicious file named aaa-evil.jsonc placed in the project directory would be used over architecture.jsonc.

Recommendation: If more than one .jsonc is found and --model was not specified, return an error requiring explicit selection.

SEC-006: No File Size Limit on JSONC Parsing (Low)

Affected file: internal/model/loader.go:13-24

Description: model.Load reads the entire file into memory with os.ReadFile without a size check. A crafted multi-gigabyte .jsonc file could exhaust memory.

Update (2026-03-30): StripJSONC now handles block comments (/* …​ */). The size limit issue remains open.

Recommendation: Add a maximum file size check (e.g., reject files over 10 MB).

SEC-007: Unbounded Recursion on Element Hierarchy (Low) — FIXED

Fix verified 2026-03-30: MaxElementDepth = 50 constant in resolve.go:11 is enforced in flattenInto, validateElement, and reverse.go (maxReverseDepth). Tests cover the depth limit.

SEC-008: stripTags Incorrect Parsing (Low — reduced)

Affected file: internal/drawio/label.go:141-155

Description: The stripTags fallback function uses a simple state machine that treats any > as a tag closer. If a draw.io label contains > inside an HTML attribute value (e.g., <font color="a>b">), the parser exits the "inTag" state prematurely, leaking attribute content into the output.

This only affects the fallback path in ParseLabel (when the label does not start with <b>).

Update (2026-03-03): With the sub-cell architecture (#232), new elements use plain-text value attributes on child <mxCell> elements instead of HTML labels. ParseLabel is now only invoked as a backward-compatible fallback for older draw.io files that lack sub-cells. This significantly reduces the attack surface of stripTags — it will only be hit when reading legacy elements.

Recommendation: Either use a proper HTML tokenizer or add attribute-aware parsing for the > inside quotes case. Lower priority now that ParseLabel is a fallback path only.

SEC-009: Outdated Go Toolchain (Medium) — FIXED

Previous version: Go 1.23.0
Fixed version: Go 1.24.0

Fix applied in PR #77: Updated go.mod to go 1.24.0. All 25 stdlib vulnerabilities resolved. Confirmed by govulncheck — no vulnerable symbols called by project code.

SEC-010: Outdated golang.org/x/sys (Low) — FIXED

Previous version: v0.13.0
Fixed version: v0.41.0

Fix applied in PR #77: Updated via go get golang.org/x/sys@latest && go mod tidy.

SEC-011: Unhandled Error in model.Save Cleanup (Low) — FIXED

Fix verified 2026-03-30: loader.go:115 now uses _ = tmp.Close() to explicitly discard the error.

SEC-012: Potential Nil Panics from Unchecked GetPage() (Low) — FIXED

Fix applied in #235: requirePage test helpers and strings.Cut refactor. Nil checks added.

SEC-015: View Key Path Traversal in Export Filenames (Medium) — FIXED

Added 2026-03-30, fixed 2026-04-27

Affected files:

  • cmd/bausteinsicht/export_diagram.go:115outPath := filepath.Join(outputDir, key+"."+ext)

  • cmd/bausteinsicht/export_table.go:75filename = viewKey + "-elements." + tableFormat

  • internal/export/export.go:53-54OutputFileName(viewKey, format)

Description: View keys from the JSONC model were used directly in output filenames without sanitization. A malicious model file could define a view key containing path separators or .. sequences:

{
  "views": {
    "../../../tmp/pwned": { "title": "Malicious View" }
  }
}

When exported with --output ./docs, this would write to ./docs/../../../tmp/pwned.puml.

Fix: Added export.SafeViewKey() which normalizes backslashes to forward slashes and applies filepath.Base() to strip directory components. Applied at all three export paths (export.go, export_diagram.go, export_table.go). The malicious key above now safely resolves to pwned.puml in the output directory.

SEC-016: No Output Directory Boundary Check (Low) — NEW

Added 2026-03-30

Affected files:

  • cmd/bausteinsicht/export_diagram.go:34--output flag

  • cmd/bausteinsicht/export_table.go:35--output flag

  • cmd/bausteinsicht/export.go:42--output flag

Description: The --output flag accepts arbitrary paths including absolute paths and paths with .. sequences. Combined with SEC-015 (view key traversal), this expands the write surface.

Risk: Low for direct CLI usage (operator controls their own flags). Medium if the CLI is wrapped in automation with untrusted input.

Recommendation: Document that --output is a fully trusted CLI flag. Optionally validate that the path is relative.

SEC-017: govulncheck Internal Error (Info) — FIXED

Added 2026-03-30, fixed 2026-04-27

Description: govulncheck ./…​ failed with:

internal error: package "golang.org/x/sys/unix" without types was imported from "github.com/fsnotify/fsnotify/internal"

This prevented automated vulnerability scanning.

Root cause: govulncheck v1.1.4 (built with Go 1.22.2) was too old to analyze the internal package structure of fsnotify v1.9.0. The type analysis engine crashed before reaching the vulnerability scan.

Fix: Updated govulncheck to v1.3.0 (Go 1.25.9) via go install golang.org/x/vuln/cmd/govulncheck@latest. Makefile and Dockerfile already used @latest — the issue was a stale local binary. Scan now passes: 0 vulnerabilities in called code.

SEC-018: Typo-Squatting in Go Module Path and Documentation (Medium) — Fixed

Added 2026-04-27

Description: The Go module path was registered as github.com/docToolchain/Bauteinsicht (missing "s"), which does not match the actual repository name Bausteinsicht. The README’s go install command and the landing page buttons also used misspelled URLs and/or the wrong GitHub owner (rdmueller instead of docToolchain).

An attacker could register the misspelled path github.com/docToolchain/Bauteinsicht and serve a malicious Go module to anyone following the go install instructions.

Risk: Medium. Users copying the go install command from the README would fetch from a path that could be claimed by a third party.

Fix: Renamed the Go module to github.com/docToolchain/Bausteinsicht across the entire codebase (go.mod, all imports, schema URLs, documentation). Corrected landing page URLs to docToolchain/Bausteinsicht.

Not Vulnerable

The following attack vectors were explicitly tested and confirmed not exploitable:

Attack Vector Reason

XXE (XML External Entity)

beevik/etree uses encoding/xml.RawToken() which does not expand entities. Go’s XML decoder never fetches external entities.

Billion Laughs / XML Bomb

Entity expansion does not occur with RawToken.

HTML Injection (forward sync)

User-controlled model strings pass through escapeHTML() before XML attribute storage. Sub-cell text values are stored as plain-text value attributes (no HTML rendering). Round-trip is safe.

JSON Deserialization Attack

Go’s encoding/json is not susceptible to unsafe deserialization. No interface{} unmarshaling of untrusted data.

Command Injection

exec.Command in export.go uses exec.LookPath for the binary and array-separated arguments — no shell interpretation. #nosec G204 justified.

Privilege Escalation

No setuid, no capability use, no privileged file paths accessed.

Supply Chain (Dependencies)

go mod verify passes. All dependency hashes match. No CVEs in beevik/etree, cobra, or fsnotify.

Dependency Status (verified 2026-03-30)

All dependencies at latest versions. No known CVEs. go mod verifyPASS.

Module Version CVEs

beevik/etree

v1.6.0

None

spf13/cobra

v1.10.2

None

fsnotify/fsnotify

v1.9.0

None

spf13/pflag

v1.0.10

None

golang.org/x/sys

v0.41.0

None

Go stdlib

1.24.0

None called

Automated Tool Results

Latest run (2026-03-30)

Tool Version Result Notes

go vet

go1.24.0

PASS

No findings

staticcheck

latest

PASS

No findings

gosec

latest

0 issues

All previous findings resolved or suppressed with justified #nosec (8 suppressions audited)

nilaway

latest

PASS

No findings (SEC-012 fixes verified)

govulncheck

v1.3.0

PASS

0 vulnerabilities in called code (SEC-017 fixed)

gitleaks

Not installed

Not available outside devcontainer; no secrets found via manual grep

go test -race

go1.24.0

PASS

No data races detected

Prioritized Action Items

Completed

SEC-002 (PR #77), SEC-003 (PR #77), SEC-009 (PR #77), SEC-010 (PR #77), SEC-012 (#235). SEC-004, SEC-007, SEC-011 verified fixed 2026-03-30 (see individual findings above).

Remaining Open

Priority ID Action

Medium

SEC-014

Deferred: upstream projects (Claude CLI, Kiro, human CLI) don’t publish checksums. Revisit when checksum support becomes available.

Next Review

The next security review should:

  • Revisit SEC-014 (Dockerfile checksum verification) — check if upstream projects now publish checksums

  • Check for new CVEs in dependencies (govulncheck now functional — SEC-017 fixed)

  • Verify gitleaks runs in CI (currently only available in devcontainer)

  • Review any new CLI commands or features added since this review