Merge pull request '[agent/codex] Security attack vector mapping. Read CLAUDE.md. Map every ex...' (#9) from agent/security-attack-vector-mapping--read-cla into dev
This commit is contained in:
commit
0ed97567fc
1 changed files with 168 additions and 0 deletions
168
docs/security-attack-vector-mapping.md
Normal file
168
docs/security-attack-vector-mapping.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Security Attack Vector Mapping
|
||||
|
||||
`CODEX.md` was not present under `/workspace`, so this mapping follows [`CLAUDE.md`](/workspace/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`, and `fs.FileInfo` getters because they are not ingress points.
|
||||
|
||||
Notes:
|
||||
- `local` is the in-repo filesystem containment layer. Its protection depends on `validatePath`, but most mutating operations still have a post-validation TOCTOU window before the final `os.*` call.
|
||||
- `workspace.Service` uses `io.Local` rooted at `/`, so its path joins are not sandboxed by this repository.
|
||||
- `datanode.FromTar` and `datanode.Restore` inherit Borg `datanode.FromTar` behavior from `forge.lthn.ai/Snider/Borg` v0.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 |
|
||||
Loading…
Add table
Reference in a new issue