Skip to content

SPOC-469: fix: harden CI pipeline, add PG18 support, and fix security issues#36

Open
danolivo wants to merge 9 commits intomainfrom
spoc-469
Open

SPOC-469: fix: harden CI pipeline, add PG18 support, and fix security issues#36
danolivo wants to merge 9 commits intomainfrom
spoc-469

Conversation

@danolivo
Copy link
Contributor

@danolivo danolivo commented Mar 10, 2026

Summary

  • Fix shell injection vulnerabilities in test/runner.py and test/t/util_test.py: replace all shell=True subprocess calls with argument lists, use Python file I/O instead of shell redirection
  • Fix Dockerfile for BuildKit + PG18: add default value to ARG PGVER for newer BuildKit compatibility, skip LLVM bitcode compilation (with_llvm=no) to avoid missing clang-19 on Alpine, remove unused clang/llvm packages
  • Add PG18 CI support: create test/t/lib/18config.env for the test matrix
  • Pin GitHub Actions to commit SHAs in both workflow files for supply chain security
  • Fix execute_sqlite_query: add missing query parameter and remove redundant f-string that masked the bug

Checklist

  • Tests added or updated (unit, regression, integration as needed) — 18config.env added for PG18 matrix
  • Docs/README updated (or not, with justification) — no user-facing doc changes needed
  • Issue is linked (branch name or URL in PR description) — SPOC-469
  • Security checks (no secrets, vulnerabilities) — shell injection and action pinning fixes are the focus of this PR
  • Breaking changes (if any) are clearly called out — none

Notes for Reviewers

  • with_llvm=no in the Dockerfile means extensions won't produce LLVM bitcode. This is intentional — Alpine postgres images don't consistently provide the matching clang version (e.g. PG18 expects clang-19 but Alpine only has an unversioned clang).
  • In runner.py, build_psql_command() now returns a list and uses -f flag instead of shell < redirection. Output capture uses Python open() instead of shell >.
  • SHA pins for GitHub Actions can be verified against their tagged releases (the original version tag is preserved in comments).
  • execute_sqlite_query() previously had no query parameter — it referenced an undefined variable. The f-string f"{query}" also masked this by deferring the NameError to call time.

Replace shell=True with argument list in subprocess.run() to prevent
potential command injection via database parameters. Use psql's -f flag
for input and Python file handling for output redirection.
@danolivo danolivo self-assigned this Mar 10, 2026
@danolivo danolivo added the enhancement New feature or request label Mar 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Warning

Rate limit exceeded

@danolivo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 24 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ce9fa4b-5e20-4005-a37d-dcc11a160b5f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c78a01 and cb0abf2.

📒 Files selected for processing (3)
  • .github/workflows/run_runner_test_harness.yml
  • .github/workflows/snowflake_regression_tests.yml
  • test/t/util_test.py
📝 Walkthrough

Walkthrough

Refactors test execution and command invocation: Python now runs psql and writes merged stdout/stderr to output files using argument lists (no shell redirection); subprocess calls for CLI helpers switched to list-style invocation; Dockerfile adds ARG PGVER=17, updates base image and drops clang/llvm; adds new test env file.

Changes

Cohort / File(s) Summary
Test runner SQL output handling
test/runner.py
Replaced shell redirection with programmatic writing of psql stdout while merging stderr; build_psql_command() now returns the psql command as an argument list rather than a shell string.
Test CLI helpers
test/t/util_test.py
Replaced shell-based subprocess.run calls with list-based argument invocation for run_cmd and run_nc_cmd (removed shell=True usage).
New test environment config
test/t/lib/18config.env
Added a comprehensive environment file exporting many test configuration variables (install script, repos, host/port, timing, node counts, dirs, credentials, versions, locale, etc.).
Dockerfile base and build changes
Dockerfile
Added ARG PGVER=17, changed FROM to postgres:${PGVER}-alpine, removed clang and llvm from apk add, and adjusted build/install to use with_llvm=no flags.

Poem

🐇 I nudge the shell aside and hop,
Args march lined up, no pipes to stop.
Python cradles stdout, stderr too,
A lighter image, build trimmed true. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title mentions multiple distinct objectives (harden CI, add PG18 support, fix security) but the PR's primary focus is fixing a shell injection vulnerability in subprocess calls. Consider a more focused title that emphasizes the primary security fix (e.g., 'Fix shell injection vulnerability in test runner subprocess calls') or confirm if all three objectives are equally important to the changeset.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly details the changeset: shell injection fixes in Python code, Dockerfile updates for PG18 compatibility, addition of test configuration, GitHub Actions pinning, and a bug fix in sqlite query execution.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch spoc-469

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/runner.py (1)

193-213: ⚠️ Potential issue | 🔴 Critical

Guard the SQL error path against UnboundLocalError.

If build_psql_command(), open(), subprocess.run(), filecmp.cmp(), or generate_diff() fails before line 17 completes, result is never assigned. The except block at lines 33–34 will crash trying to access result.stderr and result.returncode, and line 36 outside the try block will also fail when calling logTestOutput(testName, result). This converts a single test error into a runner failure.

Proposed fix
 def runTest(testName):
     extension = testName[testName.rfind('.')::]  # Extract the file extension from the test name
     if extension == '.sql':
         # Execute psql command and Compare expected/actual output files, handle any errors gracefully through try/except block
+        psql_command = []
         try:
             # Build the psql command with appropriate switches/inputs/outputs,
             psql_command, actual_output_file, expected_output_file = build_psql_command(testName)
             # Execute the psql command, redirecting output to file via Python (not shell)
             with open(actual_output_file, 'w') as outf:
                 result = subprocess.run(psql_command, stdout=outf, stderr=subprocess.STDOUT, text=True)
             # Compare actual and expected output files, setting shallow=False compares content and not just timestamps/size
             if filecmp.cmp(actual_output_file, expected_output_file, shallow=False):
                 result_status = "pass"
                 result.stdout = "Expected and Actual output files match"  # Set success message
                 result.returncode = 0  # Explicitly set the return code for success
             else:
                 result_status = "fail"
                 # Identify the diff between actual output and expected output 
                 diff_output = generate_diff(actual_output_file, expected_output_file)
                 # Set stderr to include the diff output 
                 result.stderr = "Expected and Actual output files do not match. Diff as follows:\n" + diff_output
                 result.returncode = 1 # Explicitly set the return code for failure
         except Exception as e:
             # Capture the error message
             result_status = "error"
-            result.stderr = f"An unexpected error occurred: {str(e)}"
-            result.returncode = 1  # Set return code to 1 for error
+            result = subprocess.CompletedProcess(
+                args=psql_command,
+                returncode=1,
+                stdout="",
+                stderr=f"An unexpected error occurred: {str(e)}",
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runner.py` around lines 193 - 213, The try block can leave `result`
unassigned if `build_psql_command`, `open`, `subprocess.run`, `filecmp.cmp`, or
`generate_diff` raise, causing the except block and subsequent
`logTestOutput(testName, result)` to crash; to fix, initialize a safe default
`result` before the try (e.g., a CompletedProcess-like object or plain dict with
stdout/stderr/returncode) so it always exists, then inside the except populate
`result.stderr` and `result.returncode` with the error message and non-zero
code, and ensure `logTestOutput(testName, result)` uses that guaranteed
`result`; reference the symbols build_psql_command, generate_diff, result, and
logTestOutput when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@test/runner.py`:
- Around line 193-213: The try block can leave `result` unassigned if
`build_psql_command`, `open`, `subprocess.run`, `filecmp.cmp`, or
`generate_diff` raise, causing the except block and subsequent
`logTestOutput(testName, result)` to crash; to fix, initialize a safe default
`result` before the try (e.g., a CompletedProcess-like object or plain dict with
stdout/stderr/returncode) so it always exists, then inside the except populate
`result.stderr` and `result.returncode` with the error message and non-zero
code, and ensure `logTestOutput(testName, result)` uses that guaranteed
`result`; reference the symbols build_psql_command, generate_diff, result, and
logTestOutput when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2232f22f-56e5-4757-85c5-cc53a432e55e

📥 Commits

Reviewing files that changed from the base of the PR and between 739e7b3 and f6966c3.

📒 Files selected for processing (1)
  • test/runner.py

Add default value to PGVER ARG and use ${PGVER} brace syntax in FROM.
Recent GitHub Actions runner updates ship a newer BuildKit version that
performs static validation of ARG references in FROM instructions before
build arguments are applied. Without a default value, BuildKit rejects
the Dockerfile with InvalidDefaultArgInFrom even though --build-arg
PGVER is passed at build time. The default value (17) satisfies the
static check and is overridden by the matrix build-arg in CI.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Dockerfile (1)

1-23: Static analysis: Container runs as root user.

Trivy flags that no USER directive specifies a non-root user (DS-0002). While the official postgres base image handles user switching via docker-entrypoint.sh (switching to the postgres user before starting the database), explicitly setting the user can improve security posture.

This is a pre-existing concern not introduced by this PR, so it can be addressed separately if desired.

💡 Optional: Add explicit USER directive
 ENTRYPOINT ["docker-entrypoint.sh"]
 
 CMD ["postgres"]
+
+USER postgres

Note: Adding USER postgres may require adjusting file permissions or moving it before certain commands. The entrypoint script already handles the user switch, so this is optional hardening.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 1 - 23, The Dockerfile currently never sets a
non-root USER which Trivy flags; add an explicit USER postgres (or another
non-root user) after files that need root-only permissions (e.g., after RUN
USE_PGXS=1 make install and after COPY ./ to /home/postgres/snowflake/) and
before EXPOSE/ENTRYPOINT to harden the container; if permission issues occur,
adjust ownership of the copied files and build artifacts with chown (e.g.,
change ownership of /home/postgres/snowflake to postgres) using existing RUN
steps so ENTRYPOINT ["docker-entrypoint.sh"] can run as the non-root postgres
user.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Dockerfile`:
- Around line 1-23: The Dockerfile currently never sets a non-root USER which
Trivy flags; add an explicit USER postgres (or another non-root user) after
files that need root-only permissions (e.g., after RUN USE_PGXS=1 make install
and after COPY ./ to /home/postgres/snowflake/) and before EXPOSE/ENTRYPOINT to
harden the container; if permission issues occur, adjust ownership of the copied
files and build artifacts with chown (e.g., change ownership of
/home/postgres/snowflake to postgres) using existing RUN steps so ENTRYPOINT
["docker-entrypoint.sh"] can run as the non-root postgres user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5260c421-b81a-4af9-9d49-95d6b0d42f0d

📥 Commits

Reviewing files that changed from the base of the PR and between f6966c3 and bbe20d1.

📒 Files selected for processing (1)
  • Dockerfile

PG18 Alpine image is built with --with-llvm using clang-19, but the
Alpine clang package provides an unversioned clang binary. This causes
PGXS to fail when trying to invoke clang-19 for bitcode generation.
Since snowflake does not benefit from JIT, disable LLVM bitcode
compilation with with_llvm=no and remove the now-unnecessary clang
and llvm packages from the image.
Add 18config.env for the PG18 matrix entry in the CI workflow.
Without this file, runner.py exits with an error when invoked
with -c test/t/lib/18config.env.
Same fix as runner.py: replace shell=True with argument lists in
run_cmd() and run_nc_cmd() to prevent potential command injection
via the cmd parameter.
Pin all third-party actions in both workflow files to immutable commit
SHAs instead of mutable tags, preventing potential supply chain attacks
via tag reassignment.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/t/lib/18config.env (1)

27-27: Consider using $(whoami) instead of backticks.

Modern shell best practice prefers $() over backticks for command substitution as it's more readable and nestable.

Optional style improvement
-export EDGE_REPUSER=`whoami`
+export EDGE_REPUSER=$(whoami)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/t/lib/18config.env` at line 27, Replace the backtick-style command
substitution for setting EDGE_REPUSER with the modern $() form: locate the
export statement that assigns EDGE_REPUSER (export EDGE_REPUSER=`whoami`) and
change it to use $(whoami) so the variable assignment uses the recommended
command-substitution syntax.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/t/util_test.py`:
- Around line 86-89: The run_cmd helper uses naive cmd.split() which mis-parses
quoted or space-containing arguments; import shlex and replace cmd.split() with
shlex.split(cmd) in run_cmd (function name run_cmd) so arguments are tokenized
correctly (you can also use the list-unpacking style
[f"{node_path}/pgedge/pgedge", *shlex.split(cmd)] to satisfy the RUF005 hint).
- Around line 98-101: The helper run_nc_cmd currently uses cmd.split() which
mis-parses complex shell arguments; update the subprocess.run invocation inside
run_nc_cmd to use shlex.split(cmd) instead of cmd.split(), and add an import for
shlex at the module top if missing so arguments are parsed safely when calling
subprocess.run([f"{node_path}/pgedge"] + shlex.split(cmd), ...).

---

Nitpick comments:
In `@test/t/lib/18config.env`:
- Line 27: Replace the backtick-style command substitution for setting
EDGE_REPUSER with the modern $() form: locate the export statement that assigns
EDGE_REPUSER (export EDGE_REPUSER=`whoami`) and change it to use $(whoami) so
the variable assignment uses the recommended command-substitution syntax.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0f113a6-c17c-4ab1-b4e5-260d6cf05986

📥 Commits

Reviewing files that changed from the base of the PR and between 355efcc and 0c78a01.

📒 Files selected for processing (2)
  • test/t/lib/18config.env
  • test/t/util_test.py

Add missing query parameter to function signature and remove redundant
f-string wrapper that the static analyzer flagged as potential SQL
injection.
@danolivo danolivo changed the title SPOC-469: Fix shell injection vulnerability in test runner subprocess call SPOC-469: fix: harden CI pipeline, add PG18 support, and fix security issues Mar 11, 2026
@danolivo danolivo requested a review from mason-sharp March 11, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant