Skip to content

fix: close #87-class SQLi/path-hazard gaps in rename/leave/reset/rename-team/team/api - #482

Merged
fujibee merged 2 commits into
mainfrom
fix/sqli-sibling-scripts
Jul 24, 2026
Merged

fix: close #87-class SQLi/path-hazard gaps in rename/leave/reset/rename-team/team/api#482
fujibee merged 2 commits into
mainfrom
fix/sqli-sibling-scripts

Conversation

@fujibee

@fujibee fujibee commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

join.sh's #87/PR272 fix validated and SQL-escaped agent/team names before using them, but never spliced them into a raw JSON path string. Its sibling scripts did neither, leaving the same bug class open:

  • rename.sh, leave.sh, reset.sh spliced OLD_NAME/NEW_NAME/AGENT_ID/TARGET_AGENT directly into '$.agents.<name>' JSON path literals with no validation and no escaping — a name containing a single quote could break out of the surrounding SQL statement, and one containing ., /, [, ], or " could misroute the path to the wrong key.
  • reset.sh also spliced AGENT_TYPE unescaped into a SQL literal.
  • rename-team.sh set the renamed team's $.name field from an unescaped NEW_TEAM value.
  • api.sh's get teams <team> members never validated <team> before splicing it into a filesystem path (path traversal).

Fixed by wiring in the existing agmsg_validate_agent_name/agmsg_validate_team_name gates at every entry point (mirroring join.sh), and switching every $.agents.<name> lookup to concatenate an escaped SQL string literal ('$.agents.' || '<escaped>') instead of splicing the raw name into the path text.

A deeper bug this surfaced

Fixing the above the "obvious" way would have silently inherited a second, pre-existing bug: rename.sh/leave.sh/reset.sh/rename-team.sh/team.sh all read a team's config via .param set :json '<escaped>' — but the sqlite3 shell's dot-command tokenizer does not honour SQL '' escaping (confirmed directly: .param set :x 'a''b' errors out instead of binding a'b), so this call silently mis-parsed for any config that already contained a single quote (e.g. one pre-existing agent already named with a quote), corrupting every query built on top of it. team.sh's case was worst: it printed .param's own usage help text as fake member rows, with exit code 0 (false success). Fixed every such call site to splice the (already-escaped) JSON blob as a genuine SQL string literal instead of binding it through .param set — the same pattern resolve-project.sh's resolve_team already uses for the identical reason.

Test plan

  • test_team.bats — added quote-SQLi and path-hazard-rejection tests for rename.sh, leave.sh, reset.sh, rename-team.sh, team.sh; full suite green (96/96)
  • test_api.bats — added a path-traversal rejection test for get teams <team> members; full suite green
  • test_actas_integration.bats — unaffected, still green
  • No regressions in any of the 3 bats files touching the changed scripts

fujibee added 2 commits July 23, 2026 16:02
…team/team/api

join.sh (PR272/#87) validated and SQL-escaped agent/team names but never
spliced them into a raw JSON path; its sibling scripts did neither:

- rename.sh, leave.sh, reset.sh spliced OLD_NAME/NEW_NAME/AGENT_ID/
  TARGET_AGENT directly into '$.agents.<name>' path literals with no
  validation and no escaping — a name with a single quote could break out
  of the surrounding SQL statement, and one with '.', '/', '[', ']', or
  '"' could misroute the path to the wrong key.
- reset.sh also spliced AGENT_TYPE unescaped into a SQL literal.
- rename-team.sh set the renamed team's $.name field from an unescaped
  NEW_TEAM value.
- api.sh's 'get teams <team> members' never validated <team> before
  splicing it into a filesystem path (path traversal).

Fixed by wiring in the existing agmsg_validate_agent_name/
agmsg_validate_team_name gate at every entry point, and switching every
$.agents.<name> lookup to concatenate an escaped SQL string literal
('$.agents.' || '<escaped>') rather than splicing the raw name into the
path text.

That surfaced a deeper, pre-existing bug the fix would otherwise have
silently inherited: rename.sh/leave.sh/reset.sh/rename-team.sh/team.sh all
read a team's config via '.param set :json '<escaped>'' — but the sqlite3
shell's dot-command tokenizer does not honour SQL '' escaping (confirmed
directly: '.param set :x '\'a''b\''' errors instead of binding a''b), so
this call silently mis-parsed for any config that already contained a
single quote (e.g. one existing agent already named with a quote),
corrupting every query built on top of it. team.sh's case was worst: it
printed '.param'\''s own usage text as fake member rows with exit 0. Fixed
every such call site to splice the (already-escaped) JSON blob as a
genuine SQL string literal instead of binding it via '.param set'.
fujibee added a commit that referenced this pull request Jul 24, 2026
…r bug)

The sqlite3 shell's dot-command tokenizer does not honour SQL '' escaping,
so .param set :json '<escaped>' silently mis-parses as soon as a team's
config already contains a single quote anywhere (confirmed directly:
.param set :x 'a''b' errors instead of binding a'b). Splice the escaped
JSON blob as a genuine SQL string literal in the actual SELECT statement
instead, which does honour '' escaping — same fix as PR #482 applied to
the sibling registry scripts.
fujibee added a commit that referenced this pull request Jul 24, 2026
…ndum)

koit-approved OSS interface addition for cloud/self-hosted crash recovery:
a driver's child 'connect' invocation may die between a successful
server-side exchange and the local commit finishing, leaving it unable to
tell whether that operation actually landed.

- 'remote status --json' emits a strict, secret-free object per team
  ({local_team, endpoint, server_instance_id, remote_team_id,
  credential_id, state: active|disconnected}) a driver correlates against
  its own operation-status record.
- 'remote pending list [--json]' / 'remote pending abort <pending_id>'
  enumerate and clean up an orphaned exchange that never reached a local
  commit. pending_id is the existing sha256(endpoint, token) digest
  already used as the pending record's filename — content-derived and
  opaque, so no separate generation counter is needed for ABA protection
  (reusing an id requires the identical (endpoint, token) retry). list
  always reports pending_id/endpoint even for a record whose content
  fails strict validation (a legacy/corrupt record), so crash recovery can
  still see and abort it; the raw credential is never read into the
  listing path at all.

Building this surfaced the same #87-class '.param set' tokenizer bug
already fixed in key.sh (this commit) and PR #482 (registry scripts):
_remote_read_config_field, _remote_local_disconnect, and _remote_commit
all bound the config JSON the same broken way. Fixed identically —
splice the escaped blob as a genuine SQL string literal instead of
binding it via .param set.

Tests: 17 new cases in test_remote.bats covering both JSON schemas,
never-connected/disconnected states, corrupt/legacy pending records, and
a quote-containing team name exercising the .param set fix end to end.
Full test_key.bats + test_remote.bats suite green (73/73).
@fujibee
fujibee merged commit 4672e15 into main Jul 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant