rtk-ai / rtk-ai/rtk

contains_unattestable_construct classifies heredocs as file-target redirects, so the /dev/null exemption misfires on << /dev/null

Open
#3,980 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:security bug good first issue priority:medium
Dominant language
Rust
Stars
81.1k
Forks
5.1k
Avg merge
4d 21h
Merged PRs (30d)
35

Description

Summary

contains_unattestable_construct does not distinguish a heredoc (<<, <<-) from a file-target redirect (<, >, >>). A heredoc is classified as unattestable only because its delimiter word happens to sit where a filename would, and redirect_has_file_target's /dev/null exemption — which exists for discarded output — then misfires on a heredoc whose delimiter is literally /dev/null.

No observable consequence today: registry::rewrite_command carries its own explicit has_heredoc guard (registry.rs:601,616) and refuses what slips through. This is about the predicate being right for the right reason, since it is also the permission gate's "never auto-allow" test.

Evidence

Calling the predicate directly:

            git status <<EOF -> unattestable=true
          git status <<'EOF' -> unattestable=true
           git status <<-EOF -> unattestable=true
     git status << /dev/null -> unattestable=false     <- heredoc, not a file
      git status <</dev/null -> unattestable=false     <- heredoc, not a file
        git status > out.txt -> unattestable=true
      git status > /dev/null -> unattestable=false     <- correct, this is the exemption's purpose
         git status < in.txt -> unattestable=true

The first three are true by accident: EOF reads as a file target. The two /dev/null rows are false for a reason that does not apply — nothing is being written to /dev/null; /dev/null is the delimiter marking where the inline body ends.

Why it matters

contains_unattestable_construct has three callers, and the reasoning differs for each:

  • hooks::permissions::check_command_with_rules — forces Ask so an undecomposable command is never auto-allowed. A heredoc body is inline data rather than executed text, so no bypass is known here; but the gate is currently reaching the right answer for heredocs via a filename coincidence.
  • hooks::decision::decide_with_params — refuses to rewrite. Backstopped by rewrite_command's has_heredoc.
  • discover::estimate_hook_coverage_with_verdict — mirrors the hook's decision for analytics.

A change to the /dev/null exemption, or to how a << token's operand is tokenised, moves heredoc classification as a side effect, in a predicate whose other job is a security gate.

Suggested shape

Classify the redirect operator before asking about its operand: << and <<- are heredocs (no file operand at all), everything else takes a file target and keeps the /dev/null exemption. Then heredocs are refused because they are heredocs, and the exemption applies only where a file is genuinely being written.

Worth pinning << /dev/null and <<-EOF in the lexer's own tests at the same time; today's coverage exercises the common <<EOF form only.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at contains_unattestable_construct and compare its redirect handling with the has_heredoc guard in registry.rs:601,616. Run the lexer’s own tests, then cover << /dev/null and <<-EOF alongside existing heredoc cases; done means heredocs are classified independently while the /dev/null exemption remains limited to file-target redirects.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli, security, testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.