195 lines
10 KiB
Markdown
195 lines
10 KiB
Markdown
# Security Audit
|
||
|
||
Reviewed revision: `b30c9f167dba16a9192d9bfde4aeb2b9c597ce0e`.
|
||
Audit date: 2026-10-25.
|
||
|
||
## Scope And Trust Model
|
||
|
||
This review examined input validation and budgets, import/discovery containment,
|
||
link resolution, collection authority, application callbacks, secret references
|
||
and bootstrap, historical sensitivity, diagnostics, effective rendering and
|
||
diffs, and the runtime/development dependency boundary. Knowledge-graph searches
|
||
and caller traces were supplemented by source inspection, existing tests, a
|
||
bounded standalone diagnostic, and an advisory-database spot-check.
|
||
|
||
The [architecture](../policy/architecture.md) and
|
||
[configuration contract](../config.md) establish these boundaries:
|
||
|
||
- ConfigKit is a synchronous startup library, not a network service or an
|
||
authentication/authorization framework. Applications select inputs, roots,
|
||
filesystem implementations, callbacks, and credential use. Applications own
|
||
remote acquisition and authentication of that data.
|
||
- The public facade delegates to a private engine and private document/YAML
|
||
packages. Input composition and inspection do not execute shell commands or
|
||
launch a background service. Ordinary loading does not change the process
|
||
environment; explicit secrets-directory bootstrap is the narrow exception.
|
||
- Host imports use canonical containment checks and owned rooted access under
|
||
the documented stable-input assumption. Borrowed native filesystems retain
|
||
their own behavior and are explicitly not a general sandbox guarantee.
|
||
- Secret references resolve after composition and application resolution. The
|
||
trusted returned configuration can expose credentials through `Value()`;
|
||
inspection retains names rather than credentials. Tags, a path predicate,
|
||
inheritance, and recorded dependencies govern redaction of other values.
|
||
Application-supplied messages and unrecorded secret transformations remain
|
||
application responsibilities.
|
||
|
||
One new availability finding warrants remediation. No new credential disclosure,
|
||
command injection, application-authority bypass, or host containment escape was
|
||
established. This is a source-level audit with bounded checks, not a certification
|
||
that every input, dependency, platform, or deployment is secure.
|
||
|
||
## 1. Finite Symlink Reuse Can Cause Exponential Import-Resolution Work
|
||
|
||
Status: open. Priority: medium. Category: availability / input amplification.
|
||
|
||
**Relevant code.** `resolveHostImportPath` and `resolveNativeImportPath` in
|
||
[filesystem_links.go](../../internal/engine/filesystem_links.go), lines 129–242,
|
||
expand link-target components with a frame stack and track only currently active
|
||
links. Completion removes a link from that set; a later use resolves its entire
|
||
target again. No completed-target result is reused.
|
||
|
||
`importEntryTarget` invokes these resolvers for discovered links. Both
|
||
[directory selection](../../internal/engine/import_selection.go) and
|
||
[glob selection](../../internal/engine/glob.go) reach it. Directory selection
|
||
resolves the target before deciding whether recursive descent is enabled, so
|
||
`recursive: false` does not avoid this work. Native resolution is used when the
|
||
borrowed filesystem provides `fs.ReadLinkFS`; host resolution uses the analogous
|
||
rooted implementation.
|
||
|
||
**Plausible scenario.** An application loads a configuration tree whose imported
|
||
assets can be supplied by a less-trusted user or configuration provider. That
|
||
provider controls link entries inside an allowed import directory. The
|
||
application's own main file need only select that directory with link following
|
||
enabled, which is the current default. No concurrent filesystem mutation,
|
||
outside-root access, executable callback, or malicious filesystem implementation
|
||
is required. This is conditional on influence over imported filesystem data;
|
||
it is not an anonymous remote vulnerability in ConfigKit by itself.
|
||
|
||
Inside an ordinary `links` directory, create this finite graph:
|
||
|
||
```text
|
||
s00 -> .
|
||
s01 -> s00/s00
|
||
s02 -> s01/s01
|
||
s03 -> s02/s02
|
||
...
|
||
```
|
||
|
||
Each link resolves to the same contained directory. It has no active link cycle:
|
||
the first reference to a lower link finishes before the second begins. Resolving
|
||
level N nevertheless repeats the level N-1 expansion twice, giving a recurrence
|
||
of `T(N) = 2T(N-1) + O(1)` component work for this fixed-name fixture. Directory
|
||
enumeration contains only N+1 entries, and no imported configuration nodes or
|
||
new import edges account for those repeated expansions. Consequently, source,
|
||
entry, input-node, and active-import-depth budgets do not bound this work by the
|
||
size of the selected graph. This is repeated resolution of a compact graph, not
|
||
genuinely expanding configuration output or ordinary deterministic sorting.
|
||
|
||
**Evidence.** A standalone public-API diagnostic created real temporary symlinks
|
||
and exposed them through `NewFilesystem` backed by `os.DirFS`. A thin
|
||
`fs.ReadLinkFS` wrapper counted metadata/link operations and imposed a diagnostic
|
||
ceiling of 12,000 `Lstat` calls. The main input selected `dir: links` with link
|
||
following enabled and default nonrecursive discovery. Successful loads produced
|
||
these counts:
|
||
|
||
| Link entries | `Lstat` calls | `ReadLink` calls |
|
||
| --- | ---: | ---: |
|
||
| 3 | 32 | 11 |
|
||
| 5 | 128 | 57 |
|
||
| 7 | 512 | 247 |
|
||
| 9 | 2,048 | 1,013 |
|
||
| 11 | 8,192 | 4,083 |
|
||
|
||
A 13-entry fixture reached the diagnostic ceiling and returned a safe load
|
||
failure when the wrapper stopped it. The same fixture with `follow: false`
|
||
completed with 17 `Lstat` calls and zero `ReadLink` calls. The operation counts
|
||
substantiate amplification in the native resolver; the host resolver's matching
|
||
frame-expansion logic was verified by source inspection and caller tracing,
|
||
not a separate host operation-count test. No timings, large attack graph, or
|
||
external-system exploitation were used.
|
||
|
||
**Impact.** A small imported asset tree can monopolize startup/load execution for
|
||
an impractically long period. Applications that expose configuration validation
|
||
or repeated loading to less-trusted providers can also tie up their own workers.
|
||
The normal trusted-operator-only deployment reduces the adversarial exposure,
|
||
but accidental alias graphs can cause the same availability failure.
|
||
|
||
**Potential remediation.** Reuse fully resolved link-target results within the
|
||
resolution operation, keyed by the appropriate rooted namespace and full link
|
||
path, while retaining active-path cycle detection and containment checks.
|
||
Equivalent dependency-based evaluation is another option. Preserve legitimate
|
||
finite reuse, relative/absolute host-link semantics, and the stable-input model;
|
||
do not replace the active set with a global "already visited means cycle" rule.
|
||
Review both resolvers and directory/glob entry points. The objective is to avoid
|
||
re-expanding completed dependencies, not to introduce a public cache or reject
|
||
valid graphs with an arbitrary link-count cap. Consumers can already opt out of
|
||
following discovered links where appropriate; changing that default is not part
|
||
of this recommendation.
|
||
|
||
## Previously Reported Risks
|
||
|
||
The [correctness audit](correctness.md) already records pointer-only recursive
|
||
schema nontermination, failed dynamic-descriptor cache state, and callback guards
|
||
left active during panic unwinding. The [resource audit](resources.md) owns
|
||
pre-budget typed-container allocation, incomplete retained-history accounting,
|
||
and an import-root cleanup gap after callback panic. These can affect availability
|
||
when their documented preconditions hold; they are not newly discovered remote
|
||
exploits or duplicate remediation tasks here.
|
||
|
||
The [error-handling audit](error-handling.md) separately owns silently omitted
|
||
directory contributions after certain enumeration/close failures. The
|
||
[efficiency audit](efficiency.md) owns previously identified avoidable batch costs.
|
||
This review does not reclassify every expensive operation or application
|
||
programming error as a security vulnerability.
|
||
|
||
## Dependency Spot-Check
|
||
|
||
The ordinary package's non-standard-library dependency graph contains only
|
||
`go.yaml.in/yaml/v4 v4.0.0-rc.6`; ReleaseKit, Goldmark, and `x/mod` are maintainer
|
||
tool dependencies rather than ConfigKit runtime imports. Cached module integrity
|
||
verification passed.
|
||
|
||
The current [Go vulnerability database module index](https://vuln.go.dev/index/modules.json)
|
||
contained no entries for the selected YAML v4, ReleaseKit, or Goldmark v2 module
|
||
paths. Its two `golang.org/x/mod` reports,
|
||
[GO-2026-6179](https://pkg.go.dev/vuln/GO-2026-6179) and
|
||
[GO-2026-6180](https://pkg.go.dev/vuln/GO-2026-6180), identify v0.40.0 as fixed,
|
||
which is the version selected here. The related toolchain fixes also precede the
|
||
Go 1.26.10 toolchain used for this audit. The database metadata reported a modified
|
||
time of `2026-10-21T00:27:29Z` when checked.
|
||
|
||
This was a module-path/advisory spot-check, not a complete standard-library,
|
||
toolchain, transitive-reachability, or supply-chain attestation. `govulncheck`,
|
||
`gosec`, and `gitleaks` were not installed; no scanners were added to the project.
|
||
Absence of an advisory is not proof of absence of vulnerabilities, and
|
||
`go mod verify` does not audit dependency behavior or establish that upstream
|
||
content is safe.
|
||
The [database API documentation](https://go.dev/doc/security/vuln/database)
|
||
describes the advisory source used for this check.
|
||
|
||
## Validation And Preserved Boundaries
|
||
|
||
The following passed with Go 1.26.10 on Linux amd64 using cached dependencies and
|
||
disabled automatic dependency/toolchain acquisition:
|
||
|
||
```sh
|
||
go test -race ./... -count=1
|
||
go test ./internal/engine -run 'Secret|Sensitive|Sensitivity|Contain|Escap|Outside|Symlink|Nonregular|Budget|Limit|Diagnostic' -count=1
|
||
go vet ./...
|
||
go mod verify
|
||
```
|
||
|
||
These checks include existing canary-based non-disclosure, historical and
|
||
bilateral diff sensitivity, safe diagnostic, parser-policy, containment, and
|
||
nonregular-file cases. The bounded link diagnostic ran outside the repository.
|
||
No production code or repository tests were added or changed; no native
|
||
macOS/Windows run, active external exploitation, or exhaustive fuzzing was done.
|
||
|
||
This report does not propose authentication in the library, arbitrary environment
|
||
name restrictions, secret-file permission requirements, retries, memory erasure,
|
||
or protection against privileged concurrent filesystem mutation. Those would
|
||
need their own concrete requirement and threat analysis. It preserves borrowed
|
||
filesystem ownership, application-owned safe callback messages, public secret
|
||
reference names, explicit partial bootstrap semantics, and the current release
|
||
gates.
|