13 KiB
13 KiB
Security Attack Vector Mapping
Scope: exported API entry points that accept caller-controlled input. Methods with no caller-controlled parameters ((*Store).Close, (*Store).PurgeExpired, (*ScopedStore).Namespace, EventType.String) and unexported helpers are omitted.
Cross-cutting observations:
- SQL writes and reads use placeholders throughout; no direct SQL injection sink was found in
store.go. CountAllandGroupsprefix searches escape^,%, and_before usingLIKE, which blocks wildcard injection at that sink.- Outside namespace construction, there are no length limits or character restrictions on
group,key,value, template strings, watcher filters, or callback registration. - Event payloads carry raw
group,key, andvaluestrings into watchers and callbacks without sanitisation.
Store API
| Entry point | File:line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
New(dbPath string) |
store.go:37 |
Caller-supplied SQLite path / DSN string | sql.Open("sqlite", dbPath) followed by PRAGMA execution, schema creation, and migration queries |
No path or DSN allowlist; relies on driver/open errors | Arbitrary database file selection or creation if the caller controls the path; attacker-owned or malformed DB files can force schema/migration work and denial of service on open. |
(*Store).Get(group, key string) |
store.go:89 |
Caller-supplied group and key |
Parameterised SELECT on (grp, key); if expired, parameterised DELETE; raw group+"/"+key is included in the not-found error context |
No syntax or length checks; SQL placeholders prevent direct injection | Key existence oracle; reads of expired keys become writes, which can be abused for write amplification; raw group/key strings can leak into logs or error surfaces. |
(*Store).Set(group, key, value string) |
store.go:116 |
Caller-supplied group, key, and value |
Parameterised INSERT ... ON CONFLICT DO UPDATE; persisted in SQLite; mirrored into notify(EventSet{...}) |
No syntax, size, or character validation; SQL placeholders used | Disk and memory exhaustion from unbounded values or high key churn; raw payload fan-out to watchers/callbacks can poison downstream logs or protocols; repeated same-value writes can be used to flood events. |
(*Store).SetWithTTL(group, key, value string, ttl time.Duration) |
store.go:132 |
Caller-supplied group, key, value, and ttl |
time.Now().Add(ttl).UnixMilli(); parameterised upsert; mirrored into notify(EventSet{...}) |
No TTL bounds, size limits, or character validation; SQL placeholders used | Negative or zero TTL produces immediately expired rows while still emitting EventSet; this enables event-driven side effects for data that is effectively unreadable and can create purge churn or temporary storage bloat. |
(*Store).Delete(group, key string) |
store.go:147 |
Caller-supplied group and key |
Parameterised DELETE; unconditional notify(EventDelete{...}) after the exec call |
No existence, syntax, or length checks | No-op deletes still emit delete events, so callers can spoof delete notifications for arbitrary keys; if exposed without authorisation, this is also a direct destructive surface. |
(*Store).Count(group string) |
store.go:157 |
Caller-supplied group |
Parameterised COUNT(*) query on the selected group |
No syntax or length checks | Group existence and cardinality oracle; brute-force probing can reveal tenant structure and consume DB time. |
(*Store).DeleteGroup(group string) |
store.go:170 |
Caller-supplied group |
Parameterised DELETE FROM kv WHERE grp = ?; unconditional notify(EventDeleteGroup{...}) |
No existence, syntax, or length checks | Mass-deletion surface for any reachable group; no-op deletes still emit delete_group events, which can mislead subscribers or audit trails. |
(*Store).GetAll(group string) |
store.go:185 |
Caller-supplied group |
All(group) iterator results are collected into an in-memory map[string]string |
No syntax or length checks; expired rows are filtered | Unbounded memory usage if the target group has many keys or large values; bulk exfiltration surface for the entire group. |
(*Store).All(group string) |
store.go:197 |
Caller-supplied group |
Parameterised SELECT key, value ... WHERE grp = ? when the iterator is consumed |
No syntax or length checks; placeholders used; streaming avoids one big map allocation | Full-group enumeration and sustained scan cost; attacker-controlled group population can drive long-running DB and CPU work. |
(*Store).GetSplit(group, key, sep string) |
store.go:229 |
Caller-supplied group, key, and sep |
Get(group, key) result into strings.SplitSeq(val, sep) |
Same validation as Get; no separator validation |
Same key existence oracle as Get; attacker-chosen separators can amplify token count and CPU on large stored values. |
(*Store).GetFields(group, key string) |
store.go:239 |
Caller-supplied group and key |
Get(group, key) result into strings.FieldsSeq(val) |
Same validation as Get |
Same key existence oracle as Get; large stored values can produce high CPU and large token streams. |
(*Store).Render(tmplStr, group string) |
store.go:249 |
Caller-supplied template string and group; stored keys/values in the target group |
All(group) into map[string]string, then template.New("render").Parse(tmplStr) and Execute |
No template allowlist, output size limit, or escaping mode; parse and exec errors are returned | Untrusted templates can enumerate or exfiltrate all values in the chosen group and generate unbounded output or CPU load; output is raw text/template, so downstream HTML, shell, JSON, or config injection remains possible if callers reuse it unsafely. |
(*Store).CountAll(prefix string) |
store.go:271 |
Caller-supplied group prefix | Parameterised LIKE ? ESCAPE '^' query using escapeLike(prefix) + "%", or a full-table count when prefix == "" |
escapeLike escapes ^, %, and _; SQL placeholders used |
Direct SQL wildcard injection is blocked, but empty or broad prefixes still enable whole-database counting and cross-namespace enumeration. |
(*Store).Groups(prefix string) |
store.go:293 |
Caller-supplied group prefix | GroupsSeq(prefix) results are collected into an in-memory []string |
Same escaping as CountAll |
Group-name disclosure across namespaces; unbounded memory use if there are many groups; empty prefix enumerates the whole store. |
(*Store).GroupsSeq(prefix string) |
store.go:306 |
Caller-supplied group prefix | Parameterised SELECT DISTINCT grp ... LIKE ? ESCAPE '^' when the iterator is consumed |
escapeLike escapes ^, %, and _; SQL placeholders used |
Group-name disclosure and sustained scan cost across the whole database; empty prefix scans all groups. |
Event API
| Entry point | File:line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
(*Store).Watch(group, key string) |
events.go:73 |
Caller-supplied watcher filters | Stored in Watcher{group, key} and evaluated by watcherMatches; matched events are queued on a buffered channel |
No validation; "*" is treated as wildcard by convention |
Wildcard subscriptions can observe every mutation and value; each watch allocates a buffer and adds O(n) work to every write, so unbounded watch creation is a memory and CPU denial-of-service surface; literal * names cannot be distinguished from wildcard filters. |
(*Store).Unwatch(w *Watcher) |
events.go:92 |
Caller-supplied watcher pointer | existing.id == w.id match inside the store's watcher slice, then close(w.ch) |
No nil check and no store-ownership check | nil causes a panic; passing a watcher from another Store with a colliding id can unregister a local watcher while closing the caller-supplied channel instead, causing cross-store subscription denial of service and leaving the removed watcher's channel open. |
(*Store).OnChange(fn func(Event)) |
events.go:116 |
Caller-supplied callback function | Stored in callbacks; invoked synchronously by notify() while s.mu is held for reading |
No nil check, timeout, sandboxing, or re-entrancy guard | Nil callback panics on the first mutation; slow or blocking callbacks stall writers; calling Watch, Unwatch, or OnChange from inside the callback deadlocks; callback registration is also a raw event exfiltration point. |
ScopedStore API
| Entry point | File:line | Input source | What it flows into | Current validation | Potential attack vector |
|---|---|---|---|---|---|
NewScoped(store *Store, namespace string) |
scope.go:34 |
Caller-supplied store pointer and namespace | Namespace is stored and later used by prefix() to build namespace + ":" + group names |
Namespace must match ^[a-zA-Z0-9-]+$; no nil-store or length check |
Namespace validation blocks delimiter and wildcard breakout, but a nil store still yields later panics; attacker-chosen long namespaces inflate persisted group names and event payloads. |
NewScopedWithQuota(store *Store, namespace string, quota QuotaConfig) |
scope.go:44 |
Caller-supplied store pointer, namespace, and quota config | NewScoped(...), then quota values are stored and later consumed by checkQuota() |
Same namespace regex as NewScoped; no nil-store check; negative quota values are not rejected |
Negative MaxKeys or MaxGroups effectively disable enforcement because checks only run for values > 0; nil stores still panic later if used. |
(*ScopedStore).Get(group, key string) |
scope.go:64 |
Caller-supplied group and key within a namespace |
s.prefix(group) then delegated to Store.Get(...) |
Namespace was validated at construction; group and key are otherwise unvalidated |
Same existence oracle and expired-read write amplification as Store.Get, but confined to the prefixed namespace. |
(*ScopedStore).Set(group, key, value string) |
scope.go:70 |
Caller-supplied group, key, and value within a namespace |
checkQuota(group, key) then Store.Set(s.prefix(group), key, value) |
Namespace regex at construction; quota checks only if limits are positive; underlying SQL is parameterised | Same storage and event-fan-out issues as Store.Set; quota enforcement is non-transactional, so concurrent writers can pass checkQuota() and then exceed MaxKeys or MaxGroups. |
(*ScopedStore).SetWithTTL(group, key, value string, ttl time.Duration) |
scope.go:79 |
Caller-supplied group, key, value, and ttl within a namespace |
checkQuota(group, key) then Store.SetWithTTL(s.prefix(group), key, value, ttl) |
Same as Set; no TTL bounds |
Same as Store.SetWithTTL; concurrent quota bypass remains possible, and negative or zero TTL can create immediately expired rows that fall out of future quota counts until purge, enabling bursty quota evasion and storage churn. |
(*ScopedStore).Delete(group, key string) |
scope.go:87 |
Caller-supplied group and key within a namespace |
Store.Delete(s.prefix(group), key) |
Namespace regex at construction only | Same delete-event spoofing and destructive surface as Store.Delete, limited to the namespace prefix. |
(*ScopedStore).DeleteGroup(group string) |
scope.go:92 |
Caller-supplied group within a namespace |
Store.DeleteGroup(s.prefix(group)) |
Namespace regex at construction only | Same mass-delete and no-op event spoofing as Store.DeleteGroup, limited to the namespace prefix. |
(*ScopedStore).GetAll(group string) |
scope.go:98 |
Caller-supplied group within a namespace |
Store.GetAll(s.prefix(group)) |
Namespace regex at construction only | Same bulk-read memory pressure and group-wide data exposure as Store.GetAll, limited to the namespace prefix. |
(*ScopedStore).All(group string) |
scope.go:104 |
Caller-supplied group within a namespace |
Store.All(s.prefix(group)) |
Namespace regex at construction only | Same streaming enumeration and scan-cost surface as Store.All, limited to the namespace prefix. |
(*ScopedStore).Count(group string) |
scope.go:109 |
Caller-supplied group within a namespace |
Store.Count(s.prefix(group)) |
Namespace regex at construction only | Same group-cardinality oracle as Store.Count, limited to the namespace prefix. |
(*ScopedStore).Render(tmplStr, group string) |
scope.go:115 |
Caller-supplied template string and group within a namespace |
Store.Render(tmplStr, s.prefix(group)) |
Namespace regex at construction only; same template handling as Store.Render |
Same template-driven disclosure, output injection, and resource-amplification surface as Store.Render, limited to the namespace prefix. |