Skip to content

setEnv: multiline quoted env values leak continuation lines into envResult.keys #5

Open

Description

@nathanworking

setEnv's envResult output is documented as a non-sensitive summary: "Stores only key names + byte count, never values." In practice, when the .env content passed via the env argument contains a multiline quoted value (e.g. a PEM-formatted private/public key spanning several lines, with no = on the continuation lines), the logic that derives envResult.keys appears to split the whole .env content on newlines and treat each resulting line as a candidate key token, rather than recognizing that a quoted value can itself span multiple lines. The continuation lines of the multiline value then show up as spurious entries in the keys array, positioned immediately after the real key name they belong to — meaning fragments of the secret value leak into output that is documented and relied upon as safe to log, print, and persist outside the vault boundary.

Reproduced against a production .env containing two adjacent multiline PEM-formatted values (an RSA keypair, one private key entry and one public key entry). Both produced one extra spurious entry each in envResult.keys, sitting immediately after their respective real key name and before the next real key name.

Impact: any consumer of setEnv that logs, stores, or displays envResult.keys — which is exactly what the model's own schema documentation says is safe to do — will leak line fragments of any multiline secret present in the source .env. This defeats the purpose of accepting env as a vault expression in the first place, since that design exists specifically to keep the plaintext entirely out of args/logs/model output.

Suggested fix direction: parse the .env content properly (respecting quoted multiline values as single logical entries — e.g. a real dotenv/INI-style parser, or tracking open/close-quote state across lines) before deriving the keys list, instead of a naive line split.

No key material, fragments, or vault names are included in this report.

Upstream repository: https://github.com/nathanworking/swamp-forge-provision

Environment

  • Extension: @goodcraft/forge@2026.07.21.2
  • swamp: 20260721.174127.0-sha.0e77b06b
  • OS: darwin (aarch64)
  • Deno: 2.8.3
  • Shell: /bin/zsh

Activity

  1. nathanworking commented on Aug 4, 2026

    @nathanworking
    OwnerAuthor

    Two more observations from using @goodcraft/forge across a full provision → deploy → SSL → env-rotation cycle on this same server. Neither is a security issue like the one above — filing here since they're the same extension and came up in the same work.

    1. deployLog returns truncated/unreliable output. After a deploy call reports status: "finished", calling deployLog immediately afterward has consistently returned only a single line (e.g. Cloning into 'sneak.nathanworking.com'...) instead of the full composer/npm/migrate output the deploy script actually produces. Independently verifying via SSH (build assets present and freshly timestamped, git rev-parse HEAD matching the deployed commit, migrations showing Ran, config/route caches rebuilt) confirms the deploy genuinely succeeded in every case this happened — so this looks like deployLog reading a stale or wrong log source (possibly the initial git-clone log rather than the deploy script's own output stream), not an actual deploy failure. Worth fixing since it currently makes deployLog useless for its stated purpose ("useful for diagnosing a failed deploy") — anyone trusting it at face value would wrongly distrust a successful deploy.

    2. auditSite's ready: false can be a false negative from a cosmetic string-compare. When a site is provisioned with directory: /public, a subsequent auditSite call flags directoryMatches: false and adds a "drift" entry, because it compares the raw input (/public) literally against Forge's returned absolute path (e.g. /home/forge/sneak.nathanworking.com/public) instead of checking whether the absolute path ends with the expected relative directory. The site is actually configured correctly — this makes ready: false fire even when there's nothing to fix, which undermines the method's own stated contract ("ready=true = nothing to do"). Suggest normalizing both sides (e.g. comparing on the directory's suffix, or resolving the input against the known site root) before comparing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions