pyk: path traversal family (kcfg ids, proof ids, requires, kbuild targets)

Abierto
#4,970 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
15/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
python
Área
security

Línea de trabajo

Start with the sinks listed in the table: kcfg_node_path in pyk/src/pyk/kcfg/kcfg.py, proof_data_exists and read_proof_data in pyk/src/pyk/proof/proof.py, _resolve_require in pyk/src/pyk/kast/utils.py, and the path checks in pyk/src/pyk/kbuild/project.py and kbuild.py. Each fix needs a containment check that rejects values escaping the base directory, and a test showing that a traversal value is refused. This needs a maintainer decision on the validation approach before anyone starts.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Summary

Untrusted kcfg.json/proof.json/kbuild.toml/requires values reach Path joins in kcfg.py, proof.py, kbuild/project.py, and klean; several read/write/unlink sinks.

8 finding(s) (1 MEDIUM, 7 LOW) — line-anchor verified at commit 4a46d1231473.

Findings
Severity ID Location Summary
MEDIUM pyk-core-001 pyk/src/pyk/kcfg/kcfg.py:90 Path traversal: KCFG node ids from kcfg.json are used directly as filenames
LOW pyk-core-002 pyk/src/pyk/proof/proof.py:113 Path traversal: proof id is interpolated unhashed into a path
LOW pyk-core-003 pyk/src/pyk/kast/utils.py:83 Path traversal via K requires directive (arbitrary file read/ingest)
LOW pyk-tools-001 pyk/src/pyk/kbuild/kbuild.py:39 Path traversal in KBuild.definition_dir via unvalidated target_name
LOW pyk-tools-002 pyk/src/pyk/kbuild/project.py:70 Target.__init__ accepts .. in main-file and leaves main_file unbound
LOW pyk-tools-003 pyk/src/pyk/kbuild/kbuild.py:147 _render_opt interpolates {{ project:resource }} resource name into a filesystem path
LOW pyk-tools-005 pyk/src/pyk/kbuild/project.py:102 Project source and resource directories can escape the project root via ..
LOW pyk-tools-015 pyk/src/pyk/kbuild/utils.py:91 sync_files joins caller-supplied file_names onto the target directory
Details
pyk-core-001 — Path traversal: KCFG node ids from `kcfg.json` are used directly as filenames (pyk/src/pyk/kcfg/kcfg.py:90)

Trigger (as reviewed): KCFGStore.read_cfg_data parses kcfg.json with json.loads (no schema validation) and iterates dct.get('nodes'), feeding every element straight into read_node_data(node_id) with no type or range check. read_node_data (kcfg/kcfg.py:110) calls kcfg_node_path, which builds self.kcfg_node_dir / f'{node_id}.json' (kcfg/kcfg.py:71). Because node_id is interpolated into a Path component, values such as '../../../../etc/shadow' or an absolute path escape the store directory; the .json suffix only constrains the target filename. KCFG.read_cfg_data(cfg_dir) (kcfg/kcfg.py:1298) exposes this to callers loading a KCFG from a directory they did not create (e.g. a downloaded/shared proof-artifact directory).

Impact (as reviewed): Arbitrary read of any *.json file the pyk process can read, with the parsed content then fed into KCFG.from_dict / CTerm.from_dict. Chained with 002/007 this yields local file disclosure plus attacker-selected JSON entering the deserializer.

Deep review: reachability=REACHABLE-FROM-UNTRUSTED; exploitability=EXPLOITABLE; red-team=RED-TEAM-SURVIVED.

Strongest counterargument found during red-teaming: The only durable doubt is operational rather than technical: default pyk runs never read a proof directory (save_directory defaults to None), so exploitation needs a victim who loads a directory the attacker can write, and general content disclosure additionally needs the target to be *.json and its content to be surfaced in output or logs. No code-level sanitizer or type barrier mitigates the traversal once that precondition holds.

pyk-core-002 — Path traversal: proof id is interpolated unhashed into a path (pyk/src/pyk/proof/proof.py:113)

Trigger (as reviewed): The id-hashed variant immediately above (proof.py:108) is safe, but proof_data_exists and the parallel read_proof_data (proof.py:245-259, proof/implies.py:187-191) interpolate the raw proof id as a path segment: proof_dir / id / 'proof.json'. A caller- or file-supplied id such as '../../../../tmp/x' resolves outside proof_dir. Proof.read_proof_data (line 250) also builds proof_dir / id / 'proof.json', and fetch_subproof_data (line 169) drives it from subproof_ids read out of proof JSON.

Impact (as reviewed): Arbitrary read of any proof.json on the host, and (via sibling write_proof_data paths) potential writes outside the intended proof directory. Proof ids also flow into locals()[proof_type] dispatch (006), so hostile artifacts control both the file read and the deserializer entry point.

Deep review: reachability=REACHABLE-FROM-UNTRUSTED; exploitability=EXPLOITABLE; red-team=RED-TEAM-SURVIVED.

Strongest counterargument found during red-teaming: The exploit needs a victim workflow that both sets --save-directory and treats the .k spec (or save-dir contents) as untrusted; this checkout has no such CI usage, effects are same-user, reads are limited to files named proof.json, and the Java frontend step was verified statically rather than executed end-to-end. The 'HIGH' severity is therefore arguable, but the unsafe path interpolation is confirmed and no sanitizer exists.

pyk-core-003 — Path traversal via K `requires` directive (arbitrary file read/ingest) (pyk/src/pyk/kast/utils.py:83)

Trigger (as reviewed): OuterParser.require() (kast/outer_parser.py:121-124) parses the requires "PATH" string out of a .k/.md definition with no sanitization into Require(path). slurp_definitions (kast/utils.py:42-67) then calls _resolve_require, which joins the raw string onto the definition directory and every include dir. require.path == '../../../etc/passwd' (or an absolute path) is accepted, resolved, and read by _parse_file (kast/utils.py:70-78) and parsed as K source. Nothing restricts the result to include_dirs or a project subtree.

Impact (as reviewed): When pyk parses an untrusted K definition (parse_outer, kprove, CI import of third-party specs), the attacker-chosen file is read off disk and its contents surfaced in parser error messages. Local file disclosure plus arbitrary-text-as-K-source ingestion.

pyk-tools-001 — Path traversal in `KBuild.definition_dir` via unvalidated `target_name` (pyk/src/pyk/kbuild/kbuild.py:39)

Trigger (as reviewed): target_name reaches definition_dir directly from the CLI (pyk/src/pyk/kbuild/main.py:35) and from do_which. No valid_id-style validation is applied; Project.get_target only compares names for equality. A value such as ../../../../tmp/owned resolves the output definition directory outside kdist_dir.

Impact (as reviewed): KBuild.kompile passes the resolved directory to kompile(output_dir=...), which invokes the K compiler with --output-definition . An attacker who can influence target_name causes the toolchain to create and populate a kompiled definition tree at an arbitrary writable path: an arbitrary-directory-write primitive.

Deep review: reachability=REACHABLE-FROM-UNTRUSTED; exploitability=THEORETICAL; red-team=RED-TEAM-SURVIVED.

Strongest counterargument found during red-teaming: The write requires attacker control of kbuild.toml; with that control the attacker also chooses main-file and ccopts, which reach the backend compiler via -ccopt, so the directory-write primitive may add no capability beyond the hostile-build-configuration model, and the .. variant fails to populate on a fresh tree because kdist/<version> does not exist. The absolute-target-name variant nevertheless yields a verified create-and-populate primitive.

pyk-tools-002 — `Target.__init__` accepts `..` in `main-file` and leaves `main_file` unbound (pyk/src/pyk/kbuild/project.py:70)

Trigger (as reviewed): check_relative_path rejects only absolute paths, so main-file = '../../../../etc/foo.k' passes. KBuildEnv._kompile_args (pyk/src/pyk/kbuild/kbuild.py:132) then computes self._source_dir(self.project) / args['main_file'], escaping the synced project tree. If args['main-file'] is missing the code raises KeyError; if empty/None, main_file is never assigned and line 72 raises UnboundLocalError.

Impact (as reviewed): A malicious or careless kbuild.toml in a cloned repository can point main-file at an arbitrary .k/.md file on the host, pulling it into the kompiled artifact and escaping the intended project sandbox. The unbound-variable branch masks the validation failure.

Deep review: reachability=REACHABLE-FROM-UNTRUSTED; exploitability=EXPLOITABLE; red-team=RED-TEAM-SURVIVED.

Strongest counterargument found during red-teaming: The exploit's practical impact is narrower than the finding's wording: the victim must run kbuild kompile against attacker-influenced configuration, the target must already exist as a regular file, and clean artifact inclusion requires the target to parse as K or markdown; non-K files yield only an error-message oracle. A sibling weakness (source = '../..' via relative_path, which also only rejects absolute paths) may provide an equivalent or easier read path, suggesting this may be one instance of a broader validation gap rather than a unique traversal. Additionally, the UnboundLocalError/KeyError branch is only a configuration-triggered crash, not part of the read exploit.

pyk-tools-003 — `_render_opt` interpolates `{{ project:resource }}` resource name into a filesystem path (pyk/src/pyk/kbuild/kbuild.py:147)

Trigger (as reviewed): The regex capture group resource is \S+, which matches literal ../../. _resource_dir (pyk/src/pyk/kbuild/kbuild.py:112) does self.path / project.name / 'resource' / resource_name with no normalization or containment check, so {{ p:../../../../etc/passwd }} in a target ccopts resolves out of tree.

Impact (as reviewed): The resolved path is returned into ccopts and forwarded verbatim to the LLVM backend as -ccopt . An attacker-controlled kbuild.toml can cause compiler/linker arguments to reference arbitrary host paths, enabling out-of-tree file reads and making the build depend on attacker-selected files. The exists() check only confirms existence.

pyk-tools-005 — `Project` source and resource directories can escape the project root via `..` (pyk/src/pyk/kbuild/project.py:102)

Trigger (as reviewed): relative_path rejects absolute paths only. source = '../../../../' and resources = {r = '../../../../etc'} in a kbuild.toml are accepted as long as the resolved directory exists. The resulting source_dir is used by source_files (rglob('.k')/rglob('.md')) and by KBuildEnv._sync_project, which copies matched files into the build sandbox.

Impact (as reviewed): An untrusted project definition can make pyk walk and copy arbitrary host directories into the kompiled output and include them in the build: an out-of-sandbox read primitive and a way to pull host content into a distributable artifact. check_dir_path merely verifies existence and is not containment.

Deep review: reachability=REACHABLE-FROM-UNTRUSTED; exploitability=EXPLOITABLE; red-team=RED-TEAM-SURVIVED.

Strongest counterargument found during red-teaming: The impact overstates what is proven: copied files are staged in a TemporaryDirectory that is deleted after env.kompile, and -I does not obviously copy them into the final definition. kbuild.toml is also effectively executable build configuration (post_process/ccopts), so an untrusted config may already run arbitrary commands; that makes the traversal less of a boundary crossing even though the code path itself is real.

pyk-tools-015 — `sync_files` joins caller-supplied `file_names` onto the target directory (pyk/src/pyk/kbuild/utils.py:91)

Trigger (as reviewed): file_names is a plain Iterable[str] public parameter. Current in-tree callers pass values computed via Path.relative_to (pyk/src/pyk/kbuild/project.py:175, :189), which cannot contain '..'; however the function performs no containment check on either source_file or target_file, and it is exported for reuse. A file_name of '../../../../tmp/owned' writes outside target_dir and reads outside source_dir.

Impact (as reviewed): Latent arbitrary-file-write/read via the kbuild sync primitive. LOW because present call sites are safe, but the missing guard is a half-applied sanitizer and any new caller passing config-derived names would immediately be exploitable.

Suggested fix

Reject or encode path-hostile ids/names (node ids should be ints, proof ids should be hashed consistently, requires/resource names should be confined to the definition tree) before joining them into paths.


From a static audit of this repository at commit 4a46d1231473b599c699160132fd6e76a5c46406 (master). Every location above was verified against the source at that revision; no exploit code was run against this repository or its infrastructure.

Lenguaje dominante
Python
Estrellas
605
Forks
165
Métricas de merge de PR
Sin PR fusionados en 30 d

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de runtimeverification/k

Todos los issues de runtimeverification/k

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.