Skip to content

fix: reject env-file process controls - #971

Open
mldangelo-oai wants to merge 72 commits into
mainfrom
mdangelo/codex/sec-env-startup-injection
Open

fix: reject env-file process controls#971
mldangelo-oai wants to merge 72 commits into
mainfrom
mdangelo/codex/sec-env-startup-injection

Conversation

@mldangelo-oai

Copy link
Copy Markdown
Contributor

Summary

  • parse selected repository env files into an isolated object before applying values
  • reject Node/npm, executable-resolution, loader, proxy, and config-home process controls case-insensitively
  • preserve benign application variables and documented later-file-wins behavior
  • regenerate the shipped dist/ bundle

Security impact

Fixes Codex Security finding csf_c3d60ff5bf8dd2257cd727d9 / occurrence occ_4b19ce5c6afabbbe9b229aae.

Previously, a contributor-controlled env file selected by env-files could set NODE_OPTIONS or PATH. The action loaded it directly into process.env and forwarded the environment to npx promptfoo, allowing repository code to run before Promptfoo evaluated the reviewed configuration.

The new boundary validates every selected file before any of its values can affect the action or child process.

Compatibility

Ordinary application variables, including NODE_ENV, still reach Promptfoo. Multiple selected files retain later-file-wins semantics. Trusted job-level process controls are not changed.

Validation

  • original safe end-to-end PoC is rejected before npx, with no startup observation or evaluation output
  • regression matrix covers mixed-case NODE_OPTIONS, PATH, NODE_PATH, npm config, loader, proxy, and config-home variants
  • benign multi-file environment control passes
  • npm run all passes: 9 test files, 251 tests, 100% coverage
  • post-commit npm run package leaves the working tree clean

Copy link
Copy Markdown
Contributor Author

@codex review

Please review the exact current head: fd96efa598efcd2e3b7a7a3128b7d9fdc454ba37, with particular attention to the env-file trust boundary and preserved application-variable behavior.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd96efa598

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/env.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the GitHub Action’s env-files feature by preventing repository-controlled .env files from setting process-control environment variables that could influence Node/npm startup or executable resolution before Promptfoo runs.

Changes:

  • Introduces src/utils/env.ts to load .env files into an isolated object, validate keys, then apply values to the target environment while preserving “later file wins”.
  • Updates src/main.ts to use the new guarded loader instead of calling dotenv.config() directly.
  • Updates docs/metadata (README.md, action.yml) and regenerates the bundled dist/index.js; adds regression tests for forbidden variables.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/utils/env.ts New isolated .env parsing + forbidden-key validation + controlled application to process.env.
src/main.ts Switches env loading to loadEnvironmentFile() and removes direct dotenv usage.
README.md Clarifies that env-files rejects process-control variables.
action.yml Clarifies that env-files rejects process-control variables.
tests/main.test.ts Adds tests for forbidden variable rejection (case-insensitive) and later-file-wins behavior.
dist/index.js Regenerated bundle reflecting the new env-loading implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/env.ts
Follow-up hardening on top of the env-file blocklist, closing the gaps
found while reviewing the change:

- git controls: add a GIT_ prefix (GIT_SSH_COMMAND, GIT_EXTERNAL_DIFF,
  GIT_PROXY_COMMAND, GIT_CONFIG_COUNT/KEY_n/VALUE_n, ...). The action runs
  `git diff` through simple-git after loading env-files and that child
  inherits process.env, so git controls share the Node/npm trust boundary.
- interpreter injection: reject PERL5OPT, PYTHONHOME, PYTHONEXECUTABLE,
  PYTHONSTARTUP, RUBYOPT. Module search-path vars (PYTHONPATH/RUBYLIB/
  PERL5LIB) are intentionally still allowed — real providers need them and
  they grant nothing beyond code the reviewed config already runs.
- document why validation lives at the untrusted-file boundary (the action
  and child both read merged values from process.env; the final child env
  legitimately inherits the trusted runner's PATH/NODE_OPTIONS).
- tests: new __tests__/env.test.ts exercises findForbiddenEnvFileKey and
  loadEnvironmentFile against real dotenv parsing and real files (the
  existing suite mocks dotenv), covering isolation-on-reject, later-file-
  wins, case-insensitivity, prefix rules, and load errors; extend the
  integration matrix with git/interpreter cases and a later-file rejection.
  282 tests, 100% coverage.
- README: document the workflow-env migration path for proxy/CA/process
  controls and that ordinary application vars still pass through.
- regenerate dist with the lockfile's esbuild 0.28.1 (the committed bundle
  was built with 0.28.0 and would otherwise fail check-dist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c20b8a5515

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/utils/env.ts
mldangelo and others added 2 commits July 13, 2026 21:42
…v-startup-injection

# Conflicts:
#	dist/index.js.map
…ist)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mldangelo added a commit that referenced this pull request Jul 14, 2026
Consolidate the auth-boundary protection into the shared env-file loader
introduced for the process-control fix, instead of a parallel inline check
in main.ts. loadEnvironmentFile now also rejects PROMPTFOO_API_KEY and
PROMPTFOO_REMOTE_API_BASE_URL (case-insensitively) with an auth-specific
message, so a checked-in file cannot pair an inherited credential with an
attacker-chosen host that the preflight would send the bearer token to.

- add FORBIDDEN_AUTH_KEYS + findForbiddenAuthKey to env.ts; check it in
  loadEnvironmentFile after the process-control check
- unit tests (real dotenv) for findForbiddenAuthKey and auth-key isolation
- integration tests: reject both auth vars, forward non-auth PROMPTFOO_
  settings, preserve trusted workflow authentication
- document the auth-variable rejection in the README env-files note

Stacked on the process-control fix (#971); no duplicate env-loading path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54c852b161

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/utils/env.ts

Copy link
Copy Markdown
Contributor Author

@codex review

Please re-review the latest head 0fb3a406d03fcd2cdd8096356289cbba5de364b2, including the refreshed dist/ bundle and the env-file, vault, credential-routing, and provider-endpoint hardening.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated 1 comment.

Comment thread src/main.ts Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 0fb3a406d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mldangelo-oai
mldangelo-oai requested a review from Copilot July 15, 2026 20:52
@mldangelo-oai

Copy link
Copy Markdown
Contributor Author

@codex review

Please review exact head fe20998.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 12 changed files in this pull request and generated no new comments.

@mldangelo-oai
mldangelo-oai requested a review from Copilot July 15, 2026 21:06
@mldangelo-oai

Copy link
Copy Markdown
Contributor Author

@codex review

Please review exact head 3c678b2.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 12 changed files in this pull request and generated 2 comments.

Comment thread src/utils/config.ts Outdated
Comment on lines +309 to +313
throw new PromptfooActionError(
'Invalid config directory: the config must stay within the working directory.',
ErrorCodes.INVALID_CONFIGURATION,
'Use a readable config file and directory within the working directory.',
);
Comment thread src/utils/config.ts Outdated
Comment on lines +320 to +324
throw new PromptfooActionError(
'Invalid config directory: the config must stay within the working directory.',
ErrorCodes.INVALID_CONFIGURATION,
'Use a readable config file and directory within the working directory.',
);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c678b2573

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main.ts Outdated
Comment on lines +1019 to +1020
(configGlobRepositoryRoot !== undefined &&
file.startsWith(configGlobRepositoryRoot)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don’t treat every file as a root config change

When config is a root-level glob such as *.yaml or eval*.yaml, directoryPrefix becomes empty, so configGlobRepositoryRoot is ''; because every string satisfies file.startsWith(''), any changed file in the repository is treated as a config change. In workflows that use a root config glob with prompts filtering, unrelated changes like docs/readme.md will now force a full Promptfoo evaluation instead of skipping, which can spend provider/API time unexpectedly. Restrict this fallback to root-level candidates or match the configured glob rather than using an empty prefix.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +940 to +944
if (typeof envPath === 'string') {
if (envPath.includes(',')) {
markUnsafeDependency();
} else {
processFilePath(envPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate config-level env files before eval

When a config sets commandLineOptions.envPath, this branch only records that file as a dependency; Promptfoo later loads that env file from the config during eval, outside the action's loadEnvironmentFile forbidden-key checks. In a PR that changes the config to point at .env.evil, values such as OPENAI_BASE_URL, AWS_*, or other process/provider controls can still reach the Promptfoo process and redirect trusted credentials despite the new env-file hardening. Validate these config-referenced env files before invoking Promptfoo, or reject config-level envPath in this action.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +432 to +435
const hasParentAfterMagic = pathParts
.slice(firstMagicPart)
.some((part) => part === '..');
if (hasCrossDirectoryAlternative || hasParentAfterMagic) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when glob syntax can match parent dirs

This only treats a literal .. segment as parent traversal after the first glob token, but glob syntax like [.][.] or @(.)@(.) can also match ... For a config dependency such as file://providers/[.][.]/shared/*.py, the action records only the providers/ sentinel, so a change to shared/foo.py can be missed and the evaluation skipped even though Promptfoo expands the dependency. Detect segments whose glob can match .. (as validatePromptGlob does) or conservatively return the workspace sentinel.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +670 to +671
if (extractNestedFileReferences(httpConfig.body)) {
markUnsafeDependency();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't widen on ordinary HTTP body templates

For HTTP providers whose request body contains normal Promptfoo variables, such as body: { prompt: "{{prompt}}" }, extractNestedFileReferences returns true solely because it sees the template delimiters, so this marks the whole config unsafe and returns the workspace sentinel. In workflows that otherwise monitor prompt/config files, any unrelated PR change then triggers full prompt evaluation even though the body template is just runtime data, not an untracked file dependency; only fail closed here for actual templated file references or executable hooks.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +984 to +988
(/[\\/]/.test(rawPath) &&
!path.extname(rawPath) &&
!path.extname(candidatePath))
) {
markUnsafeDependency();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore remote prompt schemes before path heuristics

Promptfoo supports external prompt references such as langfuse://... and portkey://..., but this extensionless-path fallback treats those URL schemes as local executable-style prompt paths whenever they contain slashes and no extension, marking the entire workspace as a dependency. With remote config-defined prompts, unrelated file changes now force full evaluations instead of respecting the monitored prompt/config scope; skip recognized non-file URL schemes before applying this local-path heuristic.

Useful? React with 👍 / 👎.

@mldangelo mldangelo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substantial, careful security hardening: repository .env files are now parsed into an isolated object, screened against a large process-control denylist (Node/npm/loader/proxy/etc.) before any value reaches process.env or the npx promptfoo child, and npx is isolated from a repo .npmrc via --prefix. The threat model is sound and the denylist is thorough. My comments are about minimality and test quality, not correctness of the gate:

  • The containment check (resolveContainedEnvFile) runs up to three times per env file with identical inputs, and re-implements the existing isPathInside helper twice inline with redundant realpathSync().toString()/path.resolve() wrappers.
  • Several denylist entries and a loop guard are dead code (see inline).
  • The broadened masking scans all of process.env and setSecrets any secret-named value — safe direction, but a repo-controlled env file can register a 1–2 char value as a global log mask; a min-length guard fixes that.
  • Two new tests don't exercise what they claim: the __proto__ test asserts a dotenv state real dotenv can't produce, and the npx --prefix isolation test only asserts argv shape on a mock.

None block the security fix; addressing the simplifications would meaningfully shrink the diff.

Merge/stacking note: Base main; #972 is stacked on this branch. Conflicts only with #976/#972 on README's trailing section.

Additional findings (not on changed lines, noted here):

  • 🟡 P3 · simplification — Containment validation runs up to 3× per env file; two of the three calls are pure duplicates — in src/main.ts (near resolveContainedEnvFile(envFilePath);)

    This call discards its result, then the following return resolveContainedEnvFile(effectivePath) re-validates the identical path (when no vault is selected effectivePath === envFilePath). The two load sites then validate a third time paths that are already the validated return values from this map. That triples the existsSync + 2×realpathSync syscall work and obscures where the single trust-boundary check actually lives.

Minimal fix: validate once when building the list (keep only return resolveContainedEnvFile(effectivePath);) and pass the already-validated paths to loadEnvironmentFile directly.

  • 🔵 nit · simplification — Redundant length guard around a for…of loop — in src/main.ts (near if (explicitEnvFiles.length > 0) {)

    Iterating an empty array is already a no-op, so this guard adds two lines and a nesting level for no behavior. Delete the wrapper and keep the bare for…of.

Comment thread src/main.ts Outdated
) {
throw new Error('Config path escapes the workspace');
}
realRoots.workspaceRoot ??= path.resolve(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P3 · simplification — resolveContainedEnvFile re-implements isPathInside twice, with redundant realpath wrappers

src/utils/config.ts already exports the exact three-clause containment check this inlines twice (lexical + realpath): relative === '..' || startsWith('..'+sep) || isAbsolute. Also path.resolve(fs.realpathSync(x).toString()) is doubly redundant — realpathSync(string) already returns an absolute canonical string, so both .toString() and the path.resolve() wrapper are no-ops.

Minimal fix: move/export isPathInside to a shared util and reduce this to two if (!isPathInside(base, p)) throw … checks using fs.realpathSync(...) directly (~35 lines → ~12).

Comment thread src/utils/env.ts Outdated
'CC',
'CC_HOST',
'CC_TARGET',
'CGO_CFLAGS',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P3 · simplification — Five denylist entries are dead — already covered by FORBIDDEN_ENV_FILE_PREFIXES

FORBIDDEN_ENV_FILE_PREFIXES contains 'CGO_' and 'OTEL_EXPORTER_OTLP_', and findForbiddenEnvFileKey ORs the set against the prefixes. So 'CGO_CFLAGS', 'CGO_CPPFLAGS', 'CGO_CXXFLAGS', 'CGO_LDFLAGS', and 'OTEL_EXPORTER_OTLP_ENDPOINT' in the set can never be the reason a key is rejected. In a 300-line denylist the comment says should stay "easy to audit," delete these five lines so each rule has one home.

Comment thread src/main.ts Outdated
workingDirectory: string,
pattern: string,
realRoots: { workspaceRoot?: string; workingDirectory?: string },
): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P3 · security — Broad masking lets a repo-controlled env file register a short value as a global log mask

maskApiKeys() is re-invoked after merging the implicit .env and each explicit env file, scanning all of process.env and calling core.setSecret(value) for any secret-named key. A *_TOKEN/*_SECRET/*_API_KEY key is not in the process-control denylist (those are legitimate app credentials), so an attacker's .env with e.g. X_SECRET=1 passes validation and setSecret('1') masks every 1 in the rest of the step log (eval output, cache metrics, even the action's own error messages). Over-masking is the safe direction, but this is new repo-triggerable log-integrity surface.

Minimal fix: gate the scanned branch on a minimum length (GitHub's own guidance), e.g. if (value && value.length >= 8) apiKeys.push(value);. Keep the explicit action-input keys masked unconditionally.

Comment thread __tests__/main.test.ts Outdated

await run();

const output = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P3 · tests — npx --prefix isolation test is tautological — it asserts argv shape on a mock

The test only checks args.slice(0,2) equals ['--prefix', <actionDir>] against the mocked @actions/exec; it verifies nothing about whether a repo .npmrc is actually bypassed or whether npx still resolves promptfoo with the flag inserted. The one line every run flows through has no coverage that could fail if it broke. Consider an opt-in integration test that runs the real npx --prefix <actionDir> promptfoo@<pinned> --version from a cwd containing a poisoned .npmrc, or at minimum rename the test so it doesn't claim to verify isolation.

Comment thread __tests__/main.test.ts Outdated
mockOctokit.paginate.mockResolvedValue([{ filename: 'README.md' }]);
mockGlob.sync.mockReturnValue(['prompts/first.txt']);
mockConfig.extractFileDependencies.mockReturnValue([
'data/context\n::error::forged.json',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P3 · tests — proto test asserts a dotenv state real dotenv can't produce; actual behavior is untested and contradicts the README

This test uses Object.defineProperty to force a mocked dotenv.config to emit an own enumerable __proto__ key, then asserts the run fails. Real dotenv 17.4.2 (pinned) never does this: obj['__proto__'] = 'string' is a silent no-op, so a .env with __proto__=x is silently dropped and the run proceeds — not "rejected before merging" as the README security note implies. Tellingly, the real-parser matrix in env.test.ts covers constructor/prototype but omits __proto__ (it would fail). Add a real-dotenv test asserting the silent-drop behavior and correct the README, or rename this test to state it guards a hypothetical parser.

Comment thread src/main.ts Outdated
return filePath.split(path.sep).join('/');
}

function isPathInside(baseDir: string, targetPath: string): boolean {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nit · simplification — maskApiKeys re-masks the 12 static input keys on every call

maskApiKeys() runs once at startup and again after every env-file load, rebuilding the 12-element static-input array and re-setSecreting values that can't have changed — duplicate ::add-mask:: commands. Mask the static input keys once outside the closure; keep only the process.env rescan inside.

- maskApiKeys now only registers secret-shaped values of >=8 chars. A
  repository-controlled env file could otherwise set a secret-named variable
  to a 1-2 char value and turn core.setSecret into a global log mask that
  garbles the action's own diagnostics.
- Remove CGO_CFLAGS/CGO_CPPFLAGS/CGO_CXXFLAGS/CGO_LDFLAGS and
  OTEL_EXPORTER_OTLP_ENDPOINT from FORBIDDEN_ENV_FILE_KEYS; they are already
  covered by the CGO_ and OTEL_EXPORTER_OTLP_ prefixes.
- Drop the redundant `if (explicitEnvFiles.length > 0)` guard around a
  for...of loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c341031140

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/utils/config.ts Outdated
const hasExtension =
extension.length > 0 && /^[A-Za-z0-9]+$/.test(extension);
if (/[\\/*?{}[\]]/.test(reference)) {
return !/\s/.test(reference) || /[\\/]/.test(reference) || hasExtension;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't treat prompt punctuation as a glob

When a config-defined inline prompt is a short punctuation-only string such as prompts: ['Continue?'] or {json}, this heuristic classifies it as a path glob just because it contains glob metacharacters and no whitespace. processPromptReference() then records the config directory (or ./ when the config is at the repo root) as a dependency, so unrelated file changes under that directory force a full evaluation and can spend provider/API time unexpectedly; require a path signal such as a slash, scheme, or extension before treating prompt text as a file reference.

Useful? React with 👍 / 👎.

@mldangelo

Copy link
Copy Markdown
Member

Review + fix applied (c341031)

Reviewed the current head and pushed three changes to this branch. All are behavior-preserving except the masking guard, which tightens a real (if low-severity) log-integrity issue. npm run all passes (10 test files, 1271 tests, 100% coverage) and CI is green (check-dist + CI Success).

1. Length-guard the broadened secret masking (security) — src/main.ts

maskApiKeys() now scans every process.env entry and calls core.setSecret() on any secret-shaped value. Because a *_TOKEN/*_SECRET/*_API_KEY key is not on the process-control denylist (those are legitimate application credentials), a repository-controlled .env could set e.g. X_SECRET=1 and turn core.setSecret('1') into a global mask that replaces every 1 in the rest of the step log — including the action's own diagnostics. Over-masking is the safe direction, but this is new repo-triggerable log-integrity surface, so the scan is now gated on value.length >= 8 (GitHub's own guidance). The explicit action-input keys are still masked unconditionally.

2. Remove dead denylist entries — src/utils/env.ts

FORBIDDEN_ENV_FILE_PREFIXES already contains 'CGO_' and 'OTEL_EXPORTER_OTLP_', and findForbiddenEnvFileKey ORs the set against the prefixes — so CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS, CGO_LDFLAGS, and OTEL_EXPORTER_OTLP_ENDPOINT could never be the reason a key is rejected. Removed those five lines; the matrix tests still pass because the prefixes catch them.

3. Drop a redundant guard — src/main.ts

if (explicitEnvFiles.length > 0) wrapped a for…of over the same array; iterating an empty array is already a no-op. Removed the wrapper.

Not changed (deliberately)

resolveContainedEnvFile re-implements isPathInside twice inline and runs containment validation up to 3× per env file. I left these — the safe fix is to export isPathInside from config.ts and validate once, but that's a larger refactor I didn't want to bundle with the security change. Flagging it as a good follow-up.

Note: my original inline review comments on this PR predate a branch restack and may show as "outdated"; this comment reflects the current head.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-24T07:36:30.745252Z 1059d83 New commits
🔒 Security Review Completed 2026-08-24T07:43:15.266820Z 1059d83 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc37973274

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main.ts
setupCacheEnvironment,
} from './utils/cache';
import { extractFileDependencies } from './utils/config';
import { loadEnvironmentFile } from './utils/env';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Package the env-file validation into dist

The action executes dist/index.js, but both dist/index.js and dist/index.js.map are byte-for-byte identical to the parent commit, so published users still run the old direct dotenv.config() path and none of this process-control validation takes effect. Unlike the earlier stale-source-map report, the fresh evidence at this SHA is that the executable bundle itself is also unchanged; regenerate and commit the complete dist/ output.

AGENTS.md reference: AGENTS.md:L96-L99

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: cc37973274

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/main.ts
} else {
core.info(`Successfully loaded ${envFilePath}`);
}
loadEnvironmentFile(envFilePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Security: Rebuild the action bundle with the env-file guard

When a workflow selects a checkout-controlled env-files file, rebuild dist/index.js before shipping this change. action.yml runs that bundle, but this commit leaves its blob byte-for-byte identical to the parent; it still loads directly into process.env, so this new guard never executes. A same-repository contributor can set PATH to a checked-in fake npx or use NODE_OPTIONS to execute code with job credentials. I reproduced the PATH attack. Unlike the earlier stale-source-map comment, the fresh evidence is that the executable JS itself is stale.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ac1d966ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/env.ts
Comment on lines +17 to +18
'APPDATA',
'BASH_ENV',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject AWS credential config paths

When a selected env file sets AWS_CONFIG_FILE or AWS_SHARED_CREDENTIALS_FILE to a checkout-controlled INI containing credential_process, this denylist accepts the variable and loadEnvironmentFile() forwards it to the Promptfoo child. For evaluations using an AWS provider's default credential chain, the AWS SDK can consequently execute the configured command; reject these credential-configuration path controls before merging the file environment.

AGENTS.md reference: AGENTS.md:L60-L60

Useful? React with 👍 / 👎.

Comment thread src/utils/env.ts
Comment on lines +36 to +37
'PERL5OPT',
'PYTHONEXECUTABLE',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject env-selected cache paths

When env-files contains PROMPTFOO_CACHE_PATH and the action's cache-path input is omitted, this list accepts the value, after which setupCacheEnvironment() retains it and the CI path in src/main.ts passes it to cleanupOldCache(). That function recursively deletes files older than seven days and later writes a manifest, so a repository-controlled env file can redirect cache filesystem operations outside the intended directory; reject this key or validate and confine the resulting path.

AGENTS.md reference: AGENTS.md:L60-L60

Useful? React with 👍 / 👎.

Comment thread src/utils/env.ts
// GIT_PROXY_COMMAND, GIT_CONFIG_COUNT/KEY_n/VALUE_n, ...). The action shells out
// to `git` via simple-git after loading these files, and that child inherits
// process.env, so git controls belong in the same trust boundary as Node/npm.
const FORBIDDEN_ENV_FILE_PREFIXES = ['GIT_', 'NPM_CONFIG_'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Isolate npx from the checkout .npmrc

When working-directory contains a checked-in .npmrc, rejecting NPM_CONFIG_* environment variables does not protect the later npx invocation because it runs with that directory as its cwd. npm's bundled npmrc documentation states that a per-project .npmrc at the project root sets project configuration, while the npm exec documentation states that requested packages absent locally are fetched remotely; a PR can therefore set registry to an attacker-controlled service and cause npx promptfoo@<version> to download and execute a replacement package before evaluation. Run npm with a trusted project/config location or reject checkout-controlled npm configuration.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c35d3240d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/env.ts Outdated
Comment on lines +61 to +63
const normalizedKey = key.toUpperCase();
return (
FORBIDDEN_ENV_FILE_KEYS.has(normalizedKey) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve lowercase application keys on POSIX

On Linux and macOS, environment-variable names are case-sensitive, so ordinary application settings such as path=/api/v1 or home=local do not control PATH or HOME; uppercasing every key nevertheless classifies them as forbidden and aborts the action. This regresses valid env-files usage on POSIX runners; apply case-insensitive matching only on Windows or only for controls whose consumers explicitly accept lowercase aliases.

Useful? React with 👍 / 👎.

Comment thread src/utils/env.ts
'PATH',
'PATHEXT',
'PERL5OPT',
'PROMPTFOO_CACHE_PATH',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Protect the pass-rate threshold from repository env files

When a workflow sets PROMPTFOO_PASS_RATE_THRESHOLD in its trusted environment and loads a checked-in env file, that file can override the threshold because this action-control variable is not rejected. The action later reads the overridden value in parsePromptfooPassRateThreshold() and forwards it to Promptfoo, so a PR can set it to 0 and make an evaluation that should fail the trusted threshold pass; reject this variable alongside the other protected PROMPTFOO_* controls.

Useful? React with 👍 / 👎.

Comment thread src/main.ts
} else {
core.info(`Successfully loaded ${envFilePath}`);
}
loadEnvironmentFile(envFilePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the Promptfoo auth host out of repository env files

When the workflow supplies a trusted PROMPTFOO_API_KEY, a selected repository env file can set PROMPTFOO_REMOTE_API_BASE_URL to an attacker-controlled server because that routing variable passes this validation. The subsequent authentication branch calls validatePromptfooApiKey(promptfooApiKey, getApiHost()), which sends the trusted key as a Bearer token to that URL; reject this host override from repository env files so a PR cannot exfiltrate the workflow credential before evaluation starts.

Useful? React with 👍 / 👎.

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.

3 participants