33 KiB
33 KiB
Security Attack Vector Mapping
CODEX.md was not present under /workspace, so this mapping follows CLAUDE.md and the current source tree.
Scope:
- Included: exported functions and methods that accept caller-controlled data or parse external payloads, plus public writer types returned from those APIs.
- Omitted: zero-argument accessors and teardown helpers such as
Close,Snapshot,Store,AsMedium,DataNode, andfs.FileInfogetters because they are not ingress points.
Notes:
localis the in-repo filesystem containment layer. Its protection depends onvalidatePath, but most mutating operations still have a post-validation TOCTOU window before the finalos.*call.workspace.Serviceusesio.Localrooted at/, so its path joins are not sandboxed by this repository.datanode.FromTaranddatanode.Restoreinherit Borgdatanode.FromTarbehavior fromforge.lthn.ai/Snider/Borgv0.3.1: it trims leading/, preserves symlink tar entries, and does not reject..segments or large archives.
io Facade And MockMedium
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
io.NewSandboxed |
io.go:126 |
Caller-supplied sandbox root | Delegates to local.New(root) and stores the resolved root in a local.Medium |
local.New absolutizes and best-effort resolves root symlinks; no policy check on / or broad roots |
Misconfiguration can disable containment entirely by choosing / or an overly broad root |
io.Read |
io.go:133 |
Caller path plus chosen backend | Direct m.Read(path) dispatch |
No facade-level validation | Inherits backend read, enumeration, and path-handling attack surface |
io.Write |
io.go:138 |
Caller path/content plus chosen backend | Direct m.Write(path, content) dispatch |
No facade-level validation | Inherits backend overwrite, creation, and storage-exhaustion attack surface |
io.ReadStream |
io.go:143 |
Caller path plus chosen backend | Direct m.ReadStream(path) dispatch |
No facade-level validation | Inherits backend streaming-read surface and any unbounded downstream consumption risk |
io.WriteStream |
io.go:148 |
Caller path plus chosen backend; later streamed bytes from returned writer | Direct m.WriteStream(path) dispatch |
No facade-level validation | Inherits backend streaming-write surface, including arbitrary object/file creation and unbounded buffering/disk growth |
io.EnsureDir |
io.go:153 |
Caller path plus chosen backend | Direct m.EnsureDir(path) dispatch |
No facade-level validation | Inherits backend directory-creation semantics; on no-op backends this can create false assumptions about isolation |
io.IsFile |
io.go:158 |
Caller path plus chosen backend | Direct m.IsFile(path) dispatch |
No facade-level validation | Inherits backend existence-oracle and metadata-disclosure surface |
io.Copy |
io.go:163 |
Caller-selected source/destination mediums and paths | src.Read(srcPath) loads full content into memory, then dst.Write(dstPath, content) |
Validation delegated to both backends | Large source content can exhaust memory; can bridge trust zones and copy attacker-controlled names/content across backends |
(*io.MockMedium).Read, FileGet, Open, ReadStream, List, Stat, Exists, IsFile, IsDir |
io.go:193, 225, 358, 388, 443, 552, 576, 219, 587 |
Caller path | Direct map lookup and prefix scans in in-memory maps | No normalization, auth, or path restrictions | If reused outside tests, it becomes a trivial key/value disclosure and enumeration surface |
(*io.MockMedium).Write, WriteMode, FileSet, EnsureDir |
io.go:202, 208, 230, 213 |
Caller path/content/mode | Direct map writes; WriteMode ignores mode |
No validation; permissions are ignored | Arbitrary overwrite/creation of entries and silent permission-policy bypass |
(*io.MockMedium).Create, Append, WriteStream, (*io.MockWriteCloser).Write |
io.go:370, 378, 393, 431 |
Caller path; streamed caller bytes | Buffers bytes in memory until Close, then commits to Files[path] |
No validation or size limits | Memory exhaustion and arbitrary entry overwrite if used as anything other than a test double |
(*io.MockMedium).Delete, DeleteAll, Rename |
io.go:235, 263, 299 |
Caller path(s) | Direct map mutation and prefix scans | No normalization or authorization | Arbitrary delete/rename of entries; prefix-based operations can remove more than a caller expects |
local
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
local.New |
local/client.go:24 |
Caller-supplied root path | filepath.Abs, optional filepath.EvalSymlinks, stored as Medium.root |
Absolutizes root and resolves root symlink when possible | Passing / creates unsandboxed host filesystem access; broad roots widen blast radius |
(*local.Medium).Read, FileGet |
local/client.go:114, 300 |
Caller path | validatePath then os.ReadFile |
validatePath cleans path, walks symlinks component-by-component, and blocks resolved escapes from root |
Arbitrary read of anything reachable inside the sandbox; TOCTOU symlink swap remains possible after validation and before the final read |
(*local.Medium).Open, ReadStream |
local/client.go:210, 248 |
Caller path | validatePath then os.Open; ReadStream delegates to Open |
Same validatePath containment check |
Same read/disclosure surface as Read, plus a validated path can still be swapped before os.Open |
(*local.Medium).List, Stat, Exists, IsFile, IsDir |
local/client.go:192, 201, 182, 169, 156 |
Caller path | validatePath then os.ReadDir or os.Stat |
Same validatePath containment check |
Metadata enumeration for any path inside the sandbox; TOCTOU can still skew the checked object before the final syscall |
(*local.Medium).Write, FileSet |
local/client.go:129, 305 |
Caller path/content | Delegates to WriteMode(..., 0644) |
Path containment only | Arbitrary overwrite inside the sandbox; default 0644 can expose secrets if higher layers use it for sensitive data |
(*local.Medium).WriteMode |
local/client.go:135 |
Caller path/content/mode | validatePath, os.MkdirAll, os.WriteFile |
Path containment only; caller controls file mode | Arbitrary file write inside the sandbox; caller can choose overly broad modes; TOCTOU after validation can retarget the write |
(*local.Medium).Create, WriteStream, Append |
local/client.go:219, 258, 231 |
Caller path; later bytes written through the returned *os.File |
validatePath, os.MkdirAll, os.Create or os.OpenFile(..., O_APPEND) |
Path containment only | Arbitrary truncate/append within the sandbox, unbounded disk growth, and the same post-validation race window |
(*local.Medium).EnsureDir |
local/client.go:147 |
Caller path | validatePath then os.MkdirAll |
Path containment only | Arbitrary directory creation inside the sandbox; TOCTOU race can still redirect the mkdir target |
(*local.Medium).Delete |
local/client.go:263 |
Caller path | validatePath then os.Remove |
Path containment; explicit guard blocks / and $HOME |
Arbitrary file or empty-dir deletion inside the sandbox; guard does not protect other critical paths if root is too broad; TOCTOU applies |
(*local.Medium).DeleteAll |
local/client.go:275 |
Caller path | validatePath then os.RemoveAll |
Path containment; explicit guard blocks / and $HOME |
Recursive delete of any sandboxed subtree; if the medium root is broad, the blast radius is broad too |
(*local.Medium).Rename |
local/client.go:287 |
Caller old/new paths | validatePath on both sides, then os.Rename |
Path containment on both paths | Arbitrary move/overwrite inside the sandbox; attacker-controlled rename targets can be swapped after validation |
sqlite
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
sqlite.WithTable |
sqlite/sqlite.go:29 |
Caller-supplied table name option | Stored on Medium.table and concatenated into every SQL statement |
No quoting or identifier validation | SQL injection or malformed SQL if an attacker can choose the table name |
sqlite.New |
sqlite/sqlite.go:37 |
Caller DB path/URI and options | sql.Open("sqlite", dbPath), PRAGMA, CREATE TABLE using concatenated table name |
Rejects empty dbPath; no table-name validation |
Arbitrary SQLite file/URI selection and inherited SQL injection risk from WithTable |
(*sqlite.Medium).Read, FileGet, Open, ReadStream |
sqlite/sqlite.go:94, 172, 455, 521 |
Caller path | cleanPath then parameterized SELECT; Open/ReadStream materialize the whole BLOB in memory |
Leading-slash path.Clean collapses traversal and rejects empty/root keys; path value is parameterized, table name is not |
Arbitrary logical-key read, existence disclosure, canonicalization collisions such as ../x -> x, and memory exhaustion on large BLOBs |
(*sqlite.Medium).Write, FileSet |
sqlite/sqlite.go:118, 177 |
Caller path/content | cleanPath then parameterized upsert |
Same path normalization; table name still concatenated | Arbitrary logical-key overwrite and unbounded DB growth; different raw paths can alias to the same normalized key |
(*sqlite.Medium).Create, WriteStream, Append, (*sqlite.sqliteWriteCloser).Write |
sqlite/sqlite.go:487, 546, 499, 654 |
Caller path; streamed caller bytes | cleanPath, optional preload of existing BLOB, in-memory buffering, then upsert on Close |
Non-empty normalized key only | Memory exhaustion from buffering and append preloads; arbitrary overwrite/append of normalized keys |
(*sqlite.Medium).EnsureDir |
sqlite/sqlite.go:136 |
Caller path | cleanPath then inserts a directory marker row |
Root becomes a no-op; other paths are normalized only | Arbitrary logical directory creation and aliasing through normalized names |
(*sqlite.Medium).List, Stat, Exists, IsFile, IsDir |
sqlite/sqlite.go:349, 424, 551, 155, 569 |
Caller path | cleanPath then parameterized listing/count/stat queries |
Same normalized key handling; table name still concatenated | Namespace enumeration and metadata disclosure; canonicalization collisions can hide the caller's original path spelling |
(*sqlite.Medium).Delete |
sqlite/sqlite.go:182 |
Caller path | cleanPath, directory-child count, then DELETE |
Rejects empty/root path and non-empty dirs | Arbitrary logical-key deletion |
(*sqlite.Medium).DeleteAll |
sqlite/sqlite.go:227 |
Caller path | cleanPath then DELETE WHERE path = ? OR path LIKE ? |
Rejects empty/root path | Bulk deletion of any logical subtree |
(*sqlite.Medium).Rename |
sqlite/sqlite.go:251 |
Caller old/new paths | cleanPath both paths, then transactional copy/delete of entry and children |
Requires non-empty normalized source and destination | Arbitrary move/overwrite of logical subtrees; normalized-path aliasing can redirect or collapse entries |
s3
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
s3.WithPrefix |
s3/s3.go:44 |
Caller-supplied prefix | Stored on Medium.prefix and prepended to every key |
Only ensures a trailing / when non-empty |
Cross-tenant namespace expansion or contraction if untrusted callers can choose the prefix; empty prefix exposes the whole bucket |
s3.WithClient |
s3/s3.go:55 |
Caller-supplied S3 client | Stored as Medium.client and trusted for all I/O |
No validation | Malicious or wrapped clients can exfiltrate data, fake results, or bypass expected transport controls |
s3.New |
s3/s3.go:69 |
Caller bucket name and options | Stores bucket/prefix/client on Medium |
Rejects empty bucket and missing client only | Redirecting operations to attacker-chosen buckets or prefixes if config is not trusted |
(*s3.Medium).EnsureDir |
s3/s3.go:144 |
Caller path (ignored) | No-op | Input is ignored entirely | Semantic mismatch: callers may believe a directory boundary now exists when S3 still has only object keys |
(*s3.Medium).Read, FileGet, Open |
s3/s3.go:103, 166, 388 |
Caller path | key(p) then GetObject; Read/Open read the whole body into memory |
Leading-slash path.Clean keeps the key under prefix; rejects empty key |
Arbitrary read inside the configured bucket/prefix, canonicalization collisions, and memory exhaustion on large objects |
(*s3.Medium).ReadStream |
s3/s3.go:464 |
Caller path | key(p) then GetObject, returning the raw response body |
Same normalized key handling; no size/content checks | Delivers arbitrary remote object bodies to downstream consumers without integrity, type, or size enforcement |
(*s3.Medium).Write, FileSet |
s3/s3.go:126, 171 |
Caller path/content | key(p) then PutObject |
Same normalized key handling | Arbitrary object overwrite or creation within the configured prefix |
(*s3.Medium).Create, WriteStream, Append, (*s3.s3WriteCloser).Write |
s3/s3.go:427, 481, 440, 609 |
Caller path; streamed caller bytes | key(p), optional preload of existing object for append, in-memory buffer, then PutObject on Close |
Non-empty normalized key only | Memory exhaustion from buffering and append preloads; arbitrary overwrite/append of objects under the prefix |
(*s3.Medium).List, Stat, Exists, IsFile, IsDir |
s3/s3.go:282, 355, 486, 149, 518 |
Caller path | key(p) then ListObjectsV2 or HeadObject |
Normalized key stays under prefix; no authz or tenancy checks beyond config |
Namespace enumeration and metadata disclosure across any objects reachable by the configured prefix |
(*s3.Medium).Delete |
s3/s3.go:176 |
Caller path | key(p) then DeleteObject |
Non-empty normalized key only | Arbitrary object deletion inside the configured prefix |
(*s3.Medium).DeleteAll |
s3/s3.go:193 |
Caller path | key(p), then exact delete plus prefix-based ListObjectsV2 and batched DeleteObjects |
Non-empty normalized key only | Bulk deletion of every object under a caller-chosen logical subtree |
(*s3.Medium).Rename |
s3/s3.go:252 |
Caller old/new paths | key(p) on both paths, then CopyObject followed by DeleteObject |
Non-empty normalized keys only | Arbitrary move/overwrite of objects within the configured prefix; special characters in oldPath can also make CopySource handling fragile |
store
store.Store
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
store.New |
store/store.go:22 |
Caller DB path/URI | sql.Open("sqlite", dbPath), PRAGMA, schema creation |
No validation beyond driver errors | Arbitrary SQLite file/URI selection if configuration is attacker-controlled |
(*store.Store).Get |
store/store.go:49 |
Caller group/key | Parameterized SELECT value FROM kv WHERE grp = ? AND key = ? |
Uses placeholders; no group/key policy | Arbitrary secret/config disclosure for any reachable group/key |
(*store.Store).Set |
store/store.go:62 |
Caller group/key/value | Parameterized upsert into kv |
Uses placeholders; no group/key policy | Arbitrary overwrite or creation of stored values |
(*store.Store).Delete, DeleteGroup |
store/store.go:75, 94 |
Caller group and optional key | Parameterized DELETE statements |
Uses placeholders; no authorization or namespace policy | Single-key or whole-group deletion |
(*store.Store).Count, GetAll |
store/store.go:84, 103 |
Caller group | Parameterized count or full scan of the group | Uses placeholders; no access control | Group enumeration and bulk disclosure of every key/value in a group |
(*store.Store).Render |
store/store.go:125 |
Caller template string and group name | Loads all group values into a map, then template.Parse and template.Execute |
No template allowlist or output escaping; template funcs are default-only | Template-driven exfiltration of all values in the chosen group; downstream output injection if rendered text is later used in HTML, shell, or config sinks |
store.Medium
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
store.NewMedium |
store/medium.go:23 |
Caller DB path/URI | Delegates to store.New(dbPath) |
No extra validation | Same arbitrary-DB selection risk as store.New |
(*store.Medium).EnsureDir |
store/medium.go:80 |
Caller path (ignored) | No-op | Input is ignored | Semantic mismatch: callers may assume they created a boundary when the store still treats group creation as implicit |
(*store.Medium).Read, FileGet, Open, ReadStream |
store/medium.go:62, 95, 214, 246 |
Caller medium path | splitPath then Store.Get; Open/ReadStream materialize value bytes or a string reader |
path.Clean, strip leading /, require group/key; does not forbid odd group names like .. |
Arbitrary logical-key disclosure and group/key aliasing if higher layers treat raw paths as identity |
(*store.Medium).Write, FileSet |
store/medium.go:71, 100 |
Caller path/content | splitPath then Store.Set |
Same group/key check only |
Arbitrary overwrite of any reachable group/key |
(*store.Medium).Create, WriteStream, Append, (*store.kvWriteCloser).Write |
store/medium.go:227, 259, 236, 343 |
Caller path; streamed caller bytes | splitPath, optional preload of existing value for append, in-memory buffer, then Store.Set on Close |
Requires group/key; no size limit |
Memory exhaustion and arbitrary value overwrite/append |
(*store.Medium).Delete |
store/medium.go:105 |
Caller path | splitPath; group-only paths call Count, group/key paths call Store.Delete |
Rejects empty path; refuses non-empty group deletes | Arbitrary single-key deletion and group-existence probing |
(*store.Medium).DeleteAll |
store/medium.go:124 |
Caller path | splitPath; group-only paths call DeleteGroup, group/key paths call Delete |
Rejects empty path | Whole-group deletion or single-key deletion |
(*store.Medium).Rename |
store/medium.go:136 |
Caller old/new paths | splitPath, Store.Get, Store.Set, Store.Delete |
Requires both paths to include group/key |
Arbitrary cross-group data movement and destination overwrite |
(*store.Medium).List |
store/medium.go:154 |
Caller path | Empty path lists groups; group path loads all keys via GetAll |
splitPath only; no auth |
Group and key enumeration; value lengths leak through returned file info sizes |
(*store.Medium).Stat, Exists, IsFile, IsDir |
store/medium.go:191, 264, 85, 278 |
Caller path | splitPath, then Count or Get |
Same splitPath behavior |
Existence oracle and metadata disclosure for groups and keys |
node
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
node.AddData |
node/node.go:40 |
Caller file name and content | Stores name as a map key and content as in-memory bytes |
Strips a leading /; ignores empty names and trailing /; does not clean . or .. |
Path-confusion payloads such as ../x or ./x persist verbatim and can later become traversal gadgets when copied out or tarred |
node.FromTar, (*node.Node).LoadTar |
node/node.go:84, 93 |
Caller-supplied tar archive bytes | archive/tar reader, io.ReadAll per regular file, then newFiles[name] = ... |
Trims a leading /; ignores empty names and directory entries; no path.Clean, no .. rejection, no size limits |
Tar-slip-style names survive in memory and can be exported later; huge or duplicate entries can exhaust memory or overwrite earlier entries |
(*node.Node).Read, FileGet, ReadFile, Open, ReadStream |
node/node.go:349, 370, 187, 259, 491 |
Caller path/name | Direct map lookup or directory inference; Read and ReadFile copy/convert content to memory |
Only strips a leading / |
Arbitrary access to weird literal names and confusion if callers assume canonical path handling |
(*node.Node).Write, WriteMode, FileSet |
node/node.go:359, 365, 375 |
Caller path/content/mode | Delegates to AddData; WriteMode ignores mode |
Same minimal trimming as AddData |
Arbitrary overwrite of any key, including attacker-planted ../ names; false sense of permission control |
(*node.Node).Create, WriteStream, Append, (*node.nodeWriter).Write |
node/node.go:473, 500, 480, 513 |
Caller path; streamed caller bytes | Buffer bytes in memory and commit them as a map entry on Close |
Only strips a leading /; no size limit |
Memory exhaustion and creation of path-confusion payloads that can escape on later export |
(*node.Node).Delete, DeleteAll, Rename |
node/node.go:411, 421, 445 |
Caller path(s) | Direct map mutation keyed by caller-supplied names | Only strips a leading / |
Arbitrary delete/rename of any key, including ../-style names; no directory-safe rename logic |
(*node.Node).Stat, List, ReadDir, Exists, IsFile, IsDir |
node/node.go:278, 461, 297, 387, 393, 400 |
Caller path/name | Directory inference from map keys and fs adapter methods |
Only strips a leading / |
Namespace enumeration and ambiguity around equivalent-looking path spellings |
(*node.Node).WalkNode, Walk |
node/node.go:128, 145 |
Caller root path, callback, filters | fs.WalkDir over the in-memory tree |
No root normalization beyond whatever Node already does |
Attackers who can plant names can force callback traversal over weird paths; SkipErrors can suppress unexpected failures |
(*node.Node).CopyFile |
node/node.go:200 |
Caller source key, destination host path, permissions | Reads node content and calls os.WriteFile(dst, ...) directly |
Only checks that src exists and is not a directory |
Arbitrary host filesystem write to a caller-chosen dst path |
(*node.Node).CopyTo |
node/node.go:218 |
Caller target medium, source path, destination path | Reads node entries and calls target.Write(destPath or destPath/rel, content) |
Only checks that the source exists | Stored ../-style node keys can propagate into destination paths, enabling traversal or overwrite depending on the target backend |
(*node.Node).EnsureDir |
node/node.go:380 |
Caller path (ignored) | No-op | Input is ignored | Semantic mismatch: callers may assume a directory boundary was created when directories remain implicit |
datanode
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
datanode.FromTar, (*datanode.Medium).Restore |
datanode/client.go:41, 65 |
Caller-supplied tar archive bytes | Delegates to Borg datanode.FromTar(data) and replaces the in-memory filesystem |
Wrapper adds no checks; inherited Borg behavior trims leading / only and accepts symlink tar entries |
Archive bombs, preserved symlink entries, and ../-style names can be restored into the in-memory tree |
(*datanode.Medium).Read, FileGet, Open, ReadStream |
datanode/client.go:97, 175, 394, 429 |
Caller path | clean(p) then dn.Open/dn.Stat; Read loads the full file into memory |
clean strips a leading / and runs path.Clean, but it does not sandbox .. at the start of the path |
Arbitrary logical-key reads, including odd names such as ../x; full reads can exhaust memory on large files |
(*datanode.Medium).Write, WriteMode, FileSet |
datanode/client.go:123, 138, 179 |
Caller path/content/mode | clean(p), then dn.AddData and explicit parent-dir tracking |
Rejects empty path only; WriteMode ignores mode |
Arbitrary overwrite/creation of logical entries, including ../-style names; canonicalization can also collapse some raw paths onto the same key |
(*datanode.Medium).Create, WriteStream, Append, (*datanode.writeCloser).Write |
datanode/client.go:402, 441, 410, 540 |
Caller path; streamed caller bytes | clean(p), optional preload of existing data for append, in-memory buffer, then dn.AddData on Close |
Rejects empty path; no size limit | Memory exhaustion and arbitrary overwrite/append of logical entries |
(*datanode.Medium).EnsureDir |
datanode/client.go:142 |
Caller path | clean(p) then marks explicit directories in m.dirs |
Empty path becomes a no-op; no policy on ..-style names |
Arbitrary logical directory creation and enumeration under attacker-chosen names |
(*datanode.Medium).Delete |
datanode/client.go:183 |
Caller path | clean(p), then file removal or explicit-dir removal |
Blocks deleting the empty/root path; otherwise no path policy | Arbitrary logical deletion of files or empty directories |
(*datanode.Medium).DeleteAll |
datanode/client.go:220 |
Caller path | clean(p), then subtree walk and removal |
Blocks deleting the empty/root path | Recursive deletion of any logical subtree |
(*datanode.Medium).Rename |
datanode/client.go:262 |
Caller old/new paths | clean both paths, then read-add-delete for files or subtree move for dirs |
Existence checks only; no destination restrictions | Arbitrary subtree move/overwrite, including ../-style names that later escape on export or copy-out |
(*datanode.Medium).List, Stat, Exists, IsFile, IsDir |
datanode/client.go:327, 374, 445, 166, 460 |
Caller path | clean(p), then dn.ReadDir/dn.Stat/explicit-dir map lookups |
Same non-sandboxing clean behavior |
Namespace enumeration and metadata disclosure for weird or traversal-looking logical names |
workspace
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
workspace.New |
workspace/service.go:41 |
Caller core.Core and optional cryptProvider |
Resolves $HOME, sets rootPath = ~/.core/workspaces, and binds medium = io.Local |
Ensures the root directory exists; no sandboxing because io.Local is rooted at / |
All later workspace path joins operate on the real host filesystem, not a project sandbox |
(*workspace.Service).CreateWorkspace |
workspace/service.go:68 |
Caller identifier and password | SHA-256 hashes identifier into wsID, creates directories under rootPath, calls crypt.CreateKeyPair, writes keys/private.key |
Requires crypt to exist, checks for workspace existence, writes key with mode 0600; no password policy or identifier validation |
Predictable unsalted workspace IDs can leak identifier privacy through offline guessing; creates real host directories/files if exposed remotely |
(*workspace.Service).SwitchWorkspace |
workspace/service.go:103 |
Caller workspace name | filepath.Join(rootPath, name) then medium.IsDir, stores activeWorkspace = name |
Only checks that the joined path currently exists as a directory | Path traversal via name can escape rootPath and bind the service to arbitrary host directories |
(*workspace.Service).WorkspaceFileGet |
workspace/service.go:126 |
Caller filename | activeFilePath uses filepath.Join(rootPath, activeWorkspace, "files", filename), then medium.Read |
Only checks that an active workspace is set; no filename containment check | filename can escape the files/ directory, and a malicious activeWorkspace can turn reads into arbitrary host-file access |
(*workspace.Service).WorkspaceFileSet |
workspace/service.go:138 |
Caller filename and content | Same activeFilePath join, then medium.Write |
Only checks that an active workspace is set; no filename containment check | Arbitrary host-file write if activeWorkspace or filename contains traversal segments |
(*workspace.Service).HandleIPCEvents |
workspace/service.go:150 |
Untrusted core.Message payload, typically map[string]any from IPC |
Extracts "action" and dispatches to CreateWorkspace or SwitchWorkspace |
Only loose type assertions; no schema, authz, or audit response on failure | Remote IPC callers can trigger workspace creation or retarget the service to arbitrary directories because downstream helpers do not enforce containment |
sigil
| Function | File:Line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
sigil.Transmute |
sigil/sigil.go:46 |
Caller data bytes and sigil chain | Sequential Sigil.In calls |
No chain policy; relies on each sigil | Attacker-chosen chains can trigger expensive transforms or weaken policy if callers let the attacker choose the sigils |
sigil.Untransmute |
sigil/sigil.go:62 |
Caller data bytes and sigil chain | Reverse-order Sigil.Out calls |
No chain policy; relies on each sigil | Expensive or mismatched reverse chains can become a CPU/memory DoS surface |
(*sigil.ReverseSigil).In, Out |
sigil/sigils.go:29, 41 |
Caller data bytes | Allocates a new buffer and reverses it | Nil-safe only | Large inputs allocate a second full-sized buffer; otherwise low risk |
(*sigil.HexSigil).In, Out |
sigil/sigils.go:50, 60 |
Caller data bytes | Hex encode/decode into fresh buffers | Nil-safe only; decode returns errors from hex.Decode |
Large or malformed input can still drive allocation and CPU usage |
(*sigil.Base64Sigil).In, Out |
sigil/sigils.go:74, 84 |
Caller data bytes | Base64 encode/decode into fresh buffers | Nil-safe only; decode returns errors from StdEncoding.Decode |
Large or malformed input can still drive allocation and CPU usage |
(*sigil.GzipSigil).In |
sigil/sigils.go:100 |
Caller data bytes | gzip.NewWriter, compression into a bytes.Buffer |
Nil-safe only | Large input can consume significant CPU and memory while compressing |
(*sigil.GzipSigil).Out |
sigil/sigils.go:120 |
Caller compressed bytes | gzip.NewReader then io.ReadAll |
Nil-safe only; malformed gzip errors out | Zip-bomb style payloads can decompress to unbounded memory |
(*sigil.JSONSigil).In, Out |
sigil/sigils.go:137, 149 |
Caller JSON bytes | json.Compact/json.Indent; Out is a pass-through |
No schema validation; Out does nothing |
Large inputs can consume CPU/memory; callers may wrongly assume Out validates or normalizes JSON |
sigil.NewHashSigil, (*sigil.HashSigil).In, Out |
sigil/sigils.go:161, 166, 215 |
Caller hash enum and data bytes | Selects a hash implementation, hashes input, and leaves Out as pass-through |
Unsupported hashes error out; weak algorithms are still allowed | If algorithm choice is attacker-controlled, callers can be downgraded to weak digests such as MD4/MD5/SHA1; large inputs can still be CPU-heavy |
sigil.NewSigil |
sigil/sigils.go:221 |
Caller sigil name | Factory switch returning encoding, compression, formatting, hashing, or weak hash sigils | Fixed allowlist only | If exposed as user config, attackers can select weak or semantically wrong transforms and bypass higher-level crypto expectations |
(*sigil.XORObfuscator).Obfuscate, Deobfuscate |
sigil/crypto_sigil.go:65, 73 |
Caller data and entropy bytes | SHA-256-derived keystream then XOR over a full-size output buffer | No validation | Safe only as a subroutine; if misused as standalone protection, it is merely obfuscation and still a CPU/memory surface on large input |
(*sigil.ShuffleMaskObfuscator).Obfuscate, Deobfuscate |
sigil/crypto_sigil.go:127, 154 |
Caller data and entropy bytes | Deterministic permutation and XOR-mask over full-size buffers | No validation | Large inputs drive multiple full-size allocations and CPU work; still only obfuscation if used outside authenticated encryption |
sigil.NewChaChaPolySigil |
sigil/crypto_sigil.go:247 |
Caller key bytes | Copies key into ChaChaPolySigil state |
Validates only that the key is exactly 32 bytes | Weak but correctly-sized keys are accepted; long-lived key material stays resident in process memory |
sigil.NewChaChaPolySigilWithObfuscator |
sigil/crypto_sigil.go:263 |
Caller key bytes and custom obfuscator | Builds a ChaChaPolySigil and optionally swaps the obfuscator |
Key length is validated; obfuscator is trusted if non-nil | Malicious or buggy obfuscators can break the intended defense-in-depth model or leak patterns |
(*sigil.ChaChaPolySigil).In |
sigil/crypto_sigil.go:276 |
Caller plaintext bytes | rand.Reader nonce, optional obfuscation, then chacha20poly1305.Seal |
Requires a configured key; nil input is allowed | Large plaintexts allocate full ciphertexts; if randReader is replaced in tests or DI, nonce quality becomes attacker-influenced |
(*sigil.ChaChaPolySigil).Out |
sigil/crypto_sigil.go:315 |
Caller ciphertext bytes | Nonce extraction, aead.Open, optional deobfuscation |
Requires a configured key, checks minimum length, and relies on AEAD authentication | Primarily a CPU DoS surface on repeated bogus ciphertext; integrity is otherwise strong |
sigil.GetNonceFromCiphertext |
sigil/crypto_sigil.go:359 |
Caller ciphertext bytes | Copies the first 24 bytes as a nonce | Length check only | Low-risk parser surface; malformed short inputs just error |