fix(0.30.5): close issue #269 -- 9 verified findings from security audit#270
Closed
padak wants to merge 1 commit into
Closed
fix(0.30.5): close issue #269 -- 9 verified findings from security audit#270padak wants to merge 1 commit into
padak wants to merge 1 commit into
Conversation
This release ships fixes for nine security findings surfaced by an audit driven by three sub-agents in sequence (use-case map -> state-machine -> security review). Findings A-E from issue #267 (merged in #268) are intentionally out of scope for this audit. Critical - sec-01 / sec-07: path traversal via API-controlled component_id and component_type. naming.config_path() interpolated those tokens raw, so a malicious or compromised stack returning component_id = "../../etc" would direct sync pull writes outside the workspace. Fix: new sanitize_path_segment() that rejects /, \, and parent references while preserving dots/hyphens/underscores in legitimate IDs (keboola.ex-db-mysql, kds-team.app-custom-python). Plus a defense-in-depth confinement check in sync_service.pull() that raises ConfigError if the resolved config dir is not contained in the branch dir. High - sec-02 / sec-08: MCP HTTP transport subprocess no longer inherits any KBC_* token from the kbagent process env. Pre-fix, Popen(cmd, ...) had no env= arg, so KBC_MASTER_TOKEN, KBC_MASTER_TOKEN_<ALIAS>, KBC_MANAGE_API_TOKEN, and KBC_TOKEN all leaked to the MCP server when KBAGENT_MCP_TRANSPORT=http. New _build_minimal_env() allow-lists only PATH, HOME, locale, and uv/python cache vars. Per-project Storage tokens still flow via HTTP request headers as before. Closes the v0.29.0 manage-token default-deny gap on the HTTP transport path. - sec-04: REPL history file (~/.config/keboola-agent-cli/repl_history) is now created with mode 0600. Pre-fix, FileHistory created the file with the user's umask (typically 0644), persisting any token typed at the prompt in plaintext readable by group/world. - sec-05: kbagent lineage show --format html no longer emits XSS- vulnerable HTML. render_er_diagram() previously did name.replace('"', "'") which left <, >, and & untouched -- a Keboola entity named </div><script>...</script> would inject the script. Fix uses html.escape(s, quote=True) consistently for every API-derived string. - sec-06: kbagent encrypt values --output-file now atomically creates the file with mode 0600 via os.open(..., 0o600). Replaces the previous Path.write_text() + chmod(0o600) which left a race window where the file was world-readable. Medium - sec-11: max_parallel_workers Pydantic field now requires ge=1 in addition to le=100. Pre-fix, max_parallel_workers: 0 in config.json passed validation and crashed every multi-project op with ValueError from ThreadPoolExecutor. _resolve_max_workers() also clamps defensively for legacy on-disk configs. Low - sec-19: kbagent permissions check OPERATION now reflects the EFFECTIVE policy for the invocation -- persisted policy MERGED with --deny-writes / --deny-destructive session flags -- matching permissions list semantics. Pre-fix, an AI agent inspecting its own self-imposed firewall got a misleading "allowed" answer. - sec-20: _coerce_keboola_id() and load_branch_mapping() now raise descriptive errors for malformed branch IDs in branch-mapping.json. Pre-fix, "id": "not-a-number" produced raw "invalid literal for int()" from deep inside the parser. Tests - 33 new regression tests across 7 test files. Total suite: 2830 passed (was 2797). Out of scope (tracked as follow-up in #269) - sec-03 token-in-argv deprecation - sec-09 PTY-bypass design - sec-10 silent name collisions - sec-12 version_cache atomicity - sec-13 @file path restriction - sec-14 alias validation - sec-16 SRI on Mermaid CDN - sec-17 KBAGENT_AUTO_UPDATE toggle staleness - sec-18 doctor --fix uv via PATH Audit artifacts kept locally at /tmp/kbagent-audit/use-cases.md (28K) + state-machine.md (43K) + issues.md (15K) for the test design phase.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #269.
Fixes nine verified security findings surfaced by a three-stage automated audit (use-case map → state-machine → security review). Bugs A–E from #267 were intentionally out of scope. Each finding was verified by reading code; the eight non-trivial ones have explicit regression tests.
Summary
component_id/component_typeinsync pullsync/naming.py:25-28+services/sync_service.py:434KBC_MASTER_TOKEN*andKBC_MANAGE_API_TOKENservices/mcp_transport.py:115commands/repl.py:63lineage show --format html(entity / config names)services/deep_lineage_service.py:1156, 1177, 1204, 1217-1218, 1227encrypt values --output-filerace (world-readable window before chmod)commands/encrypt.py:107-108max_parallel_workers: 0crashes ThreadPoolExecutor (noge=1)models.py:76-79permissions checkignores--deny-writes/--deny-destructiveflagscommands/permissions.py:367_coerce_keboola_idraises rawValueErroron non-numeric IDsync/branch_mapping.py:24Changes
sec-01 + sec-07 — path traversal
naming.config_path()previously interpolatedcomponent_typeandcomponent_idraw, while onlyconfig_namewas sanitized. With template{component_type}/{component_id}/{config_name}, an API-controlledcomponent_id = "../../../etc"would routesync pullwrites outside the workspace.sync/naming.py:sanitize_path_segment()rejects/,\, and parent-directory references (..) while preserving dots, hyphens, underscores. Legitimate component IDs (keboola.ex-db-mysql,kds-team.app-custom-python,keboola.python-transformation-v2) pass through unchanged.component_typeandcomponent_idnow go through it inconfig_path()._ensure_within_branch()inservices/sync_service.pyresolvesbranch_dirandconfig_dirand raisesConfigErrorif the resolved config dir is not contained in the branch dir. Called before any file write.sec-02 + sec-08 — MCP HTTP env scrubbing
The HTTP transport at
services/mcp_transport.py:_start()usedsubprocess.Popen(cmd, ...)with noenv=argument → MCP server inherited the full kbagent environment. The stdio transport atmcp_service.py:_build_server_params()was already safe (explicit minimal env via MCP SDK'sget_default_environment()); only the HTTP transport leaked._build_minimal_env()allow-lists only what the MCP binary needs:PATH,HOME,USER,LOGNAME,TERM,SHELL,TMPDIR/TEMP/TMP,LANG,LC_ALL,LC_CTYPE,PYTHONPATH/PYTHONHOME/VIRTUAL_ENV,UV_CACHE_DIR/UV_PYTHON,XDG_*. EveryKBC_*is omitted.sec-04 — REPL history 0600
commands/repl.py:_get_history_path()now atomically creates the file withos.open(O_WRONLY|O_CREAT|O_EXCL, 0o600)and tightens existing files viachmod(0o600)(silently no-op on filesystems that reject chmod, e.g. some network mounts).sec-05 — HTML XSS
services/deep_lineage_service.py:render_er_diagram()previously didname.replace('"', "'")for entity/config names, leaving<,>, and&untouched. A Keboola table named</div><script>alert(1)</script>would inject the script into the generated HTML body where the browser parses it before Mermaid runs.Fix:
html.escape(s, quote=True)for every API-derived string inrender_er_diagram(). Mermaid renders entities back to their characters in SVG text so visible output is unchanged.sec-06 — atomic encrypt output
commands/encrypt.pyreplacedoutput_file.write_text(...)+output_file.chmod(0o600)withos.open(path, O_WRONLY|O_CREAT|O_TRUNC, 0o600)+os.write(). No race window between create and chmod.sec-11 — max_parallel_workers ge=1
Added
ge=1to the Pydantic Field inmodels.py.BaseService._resolve_max_workers()also clamps to>= 1defensively (so a legacy on-disk config with0does not break startup before the validator runs).sec-19 — permissions check applies session flags
commands/permissions.py:permissions_check()now usesapply_firewall_flags()the same waypermissions listdoes, sokbagent --deny-writes permissions check branch.createcorrectly returns DENIED.sec-20 — descriptive ValueError
sync/branch_mapping.py:_coerce_keboola_id()now wrapsint(raw)intry/exceptand raises a descriptiveValueErrornaming the bad value.load_branch_mapping()re-wraps with the file path so users seeFailed to parse /path/to/branch-mapping.json: Invalid branch ID 'not-a-number'.Tests
33 new regression tests:
tests/test_sync_naming.pyTestSanitizePathSegment(4) +TestConfigPathTraversalRegression(2)tests/test_mcp_transport.pyTestBuildMinimalEnv(6)tests/test_repl.pyTestHistoryPathPermissions(2)tests/test_deep_lineage_service.pyTestRenderErDiagramXssRegression(2)tests/test_models.pyTestMaxParallelWorkersValidation(2)tests/test_sync_branch_mapping.pyTestBranchMappingandTestBranchMappingIO(2)tests/test_permissions_cli.pyTestPermissionsCheck(2)Total suite: 2830 passed (was 2797).
Test plan
make lint format-check skill-check changelog-check check-error-codesuv run pytest tests/ --ignore=tests/test_e2e.py --ignore=tests/test_sync_e2e.py --ignore=tests/test_e2e_lineage_deep.py→ 2830 passedmainbefore the fixsanitize_path_segment()on adversarial inputs (../../etc,foo/../bar,.., etc.)_build_minimal_env()does not include anyKBC_*even when those vars are sethtml.escaperenders Mermaid output as expectedOut of scope (tracked as follow-ups in #269)
--tokenargv visible tops. Design change (deprecation cycle for the flag).permissions reset/setPTY bypass. Design change (second-factor / parent-process check).sanitize_name. UX warning, not security.version_cache.jsonnon-atomic write race.--input @filereads arbitrary path. Design discussion needed.KBC_MASTER_TOKEN_{ALIAS}from unvalidated alias.KBAGENT_AUTO_UPDATEtoggle stale cache.doctor --fixuses PATH-resolveduv.Each will get its own follow-up issue if/when prioritized.
Audit artifacts
The audit produced three structured documents (kept locally for the test design phase):
/tmp/kbagent-audit/use-cases.md— 28 KB — every command + 20 life situations + cross-cutting concerns/tmp/kbagent-audit/state-machine.md— 43 KB — every persistent / in-memory state with file:line, command-by-command read/write/assert table, life-situation traces, 9 forbidden state combinations, concurrency model, 6 Mermaid state diagrams/tmp/kbagent-audit/issues.md— 15 KB — the 20 findings with file:line, repro, exploitability, suggested fix, confidence