Skip to content

Commit cebd651

Browse files
committed
security: add ClusterFuzzLite fuzzing for the SQL-safety kernel
Closes Scorecard Fuzzing alert #55. Scorecard's Fuzzing check does not recognize Python source patterns at all (only Go/Haskell/JS-TS/Erlang); the only path to real credit for a Python project is OSS-Fuzz or ClusterFuzzLite deployment. Confirmed via the alert's own rule.help text before building this, per the standard OSS-Fuzz Python integration (https://google.github.io/oss-fuzz/getting-started/new-project-guide/python-lang/). Harness (.clusterfuzzlite/sql_safety_fuzzer.py) targets SafeSqlDriver._validate — the pglast parse + AST-walker in mcpg.sql.safety — feeding it arbitrary bytes via Atheris and treating anything but the documented ValueError as a bug. This is the project's actual security-critical surface (see CLAUDE.md's "SQL-safety kernel" section); the existing tests/unit/test_sql_kernel_fuzz.py property tests cover known-shape adversarial inputs, this harness covers unknown-shape ones. Two workflows: cflite_pr.yml (5-minute fuzz run on PRs touching src/mcpg/sql/** or .clusterfuzzlite/**, address+undefined sanitizers) and cflite_batch.yml (1-hour daily batch run to build corpus depth over time). Also adds .gitattributes rules forcing LF for *.sh and .clusterfuzzlite/** — these scripts run inside a Linux container and a CRLF-mangled shebang would break the interpreter on a Windows checkout. Verified: Atheris 3.1.0 ships wheels for cp312/cp313/cp314, matching this project's requires-python >=3.12 — no version-compatibility gap on the fuzzing library itself. Full build/run validated by this PR's own cflite_pr.yml workflow run (Docker-in-Docker local validation wasn't available in this session — see PR checks for the real signal). Entire-Checkpoint: 9d5df8697fd3
1 parent 03c7f07 commit cebd651

7 files changed

Lines changed: 155 additions & 0 deletions

File tree

.clusterfuzzlite/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ClusterFuzzLite build integration for the SQL-safety kernel
2+
# (mcpg.sql.safety). See docs/reviews/devendor-sql-kernel-security-review.md
3+
# for why this parser+AST-walker is the project's security-critical
4+
# surface. Atheris and the OSS-Fuzz Python toolchain are pre-installed on
5+
# this base image — see
6+
# https://google.github.io/oss-fuzz/getting-started/new-project-guide/python-lang/
7+
FROM gcr.io/oss-fuzz-base/base-builder-python
8+
9+
COPY . $SRC/mcpg
10+
WORKDIR $SRC/mcpg
11+
COPY .clusterfuzzlite/build.sh $SRC/
12+
COPY .clusterfuzzlite/sql_safety_fuzzer.py $SRC/

.clusterfuzzlite/build.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash -eu
2+
# Standard OSS-Fuzz Python build script — see
3+
# https://google.github.io/oss-fuzz/getting-started/new-project-guide/python-lang/
4+
# for what each step does and why.
5+
6+
# Build and install mcpg (using current CFLAGS/CXXFLAGS) so its C-extension
7+
# deps (pglast wraps libpg_query; psycopg's C extension) are compiled with
8+
# the sanitizer this run is instrumented with.
9+
pip3 install .
10+
11+
for fuzzer in $(find "$SRC" -name '*_fuzzer.py'); do
12+
fuzzer_basename=$(basename -s .py "$fuzzer")
13+
fuzzer_package=${fuzzer_basename}.pkg
14+
15+
# Standalone package via pyinstaller, to avoid Python-version/environment
16+
# drift between build time and whenever ClusterFuzzLite replays this
17+
# binary later.
18+
pyinstaller --distpath "$OUT" --onefile --name "$fuzzer_package" "$fuzzer"
19+
20+
# Execution wrapper: Atheris needs the sanitizer runtime preloaded, and
21+
# this is the file ClusterFuzzLite actually invokes as the fuzz target.
22+
echo "#!/bin/sh
23+
this_dir=\$(dirname \"\$0\")
24+
LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
25+
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
26+
\$this_dir/$fuzzer_package \$@" > "$OUT/$fuzzer_basename"
27+
chmod +x "$OUT/$fuzzer_basename"
28+
done

.clusterfuzzlite/project.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
language: python3
2+
fuzzing_engines:
3+
- libfuzzer
4+
sanitizers:
5+
- address
6+
- undefined
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Atheris fuzz harness for the SQL-safety kernel's parse+validate path.
2+
3+
Feeds arbitrary bytes to ``SafeSqlDriver._validate`` — the ``pglast``-based
4+
parser + AST-walker in ``mcpg.sql.safety`` — and treats anything other than
5+
the documented ``ValueError`` (malformed or disallowed SQL) as a bug: an
6+
unhandled exception, a native crash inside the C-based ``pglast`` parser, or
7+
a hang. This is the project's actual security-critical surface; see
8+
CLAUDE.md's "SQL-safety kernel" section and
9+
docs/reviews/devendor-sql-kernel-security-review.md for the threat model
10+
this complements (that doc covers the adversarial *unit* test suite —
11+
known-shape attacks; this harness covers unknown-shape inputs).
12+
"""
13+
14+
import sys
15+
16+
import atheris
17+
18+
with atheris.instrument_imports():
19+
from mcpg.sql.safety import SafeSqlDriver
20+
21+
# _validate() only reads the class-level ALLOWED_* policy aliases; the
22+
# wrapped driver is never touched during validation, so a real SqlDriver
23+
# is unnecessary here.
24+
_driver = SafeSqlDriver(sql_driver=None) # type: ignore[arg-type]
25+
26+
27+
def test_one_input(data: bytes) -> None:
28+
fdp = atheris.FuzzedDataProvider(data)
29+
query = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())
30+
try:
31+
_driver._validate(query) # fuzzing the private validator directly
32+
except ValueError:
33+
pass # expected: malformed or policy-disallowed SQL is rejected
34+
35+
36+
atheris.Setup(sys.argv, test_one_input)
37+
atheris.Fuzz()

.gitattributes

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
demo_data/*.sql filter=lfs diff=lfs merge=lfs -text
2+
3+
# Scripts that run inside Linux containers (ClusterFuzzLite build, CI) must
4+
# keep LF endings regardless of the checkout platform's core.autocrlf — a
5+
# stray \r in a shebang line breaks the interpreter.
6+
*.sh text eol=lf
7+
.clusterfuzzlite/** text eol=lf

.github/workflows/cflite_batch.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: ClusterFuzzLite batch fuzzing
2+
on:
3+
schedule:
4+
- cron: "0 3 * * *" # Daily at 03:00 UTC.
5+
permissions: read-all
6+
jobs:
7+
BatchFuzzing:
8+
runs-on: ubuntu-latest
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
sanitizer:
13+
- address
14+
- undefined
15+
steps:
16+
- name: Build Fuzzers (${{ matrix.sanitizer }})
17+
id: build
18+
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
19+
with:
20+
language: python
21+
sanitizer: ${{ matrix.sanitizer }}
22+
- name: Run Fuzzers (${{ matrix.sanitizer }})
23+
id: run
24+
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
25+
with:
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
fuzz-seconds: 3600
28+
mode: "batch"
29+
sanitizer: ${{ matrix.sanitizer }}
30+
output-sarif: true

.github/workflows/cflite_pr.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: ClusterFuzzLite PR fuzzing
2+
on:
3+
pull_request:
4+
paths:
5+
- "src/mcpg/sql/**"
6+
- ".clusterfuzzlite/**"
7+
permissions: read-all
8+
jobs:
9+
PR:
10+
runs-on: ubuntu-latest
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
sanitizer:
18+
- address
19+
- undefined
20+
steps:
21+
- name: Build Fuzzers (${{ matrix.sanitizer }})
22+
id: build
23+
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
24+
with:
25+
language: python
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
sanitizer: ${{ matrix.sanitizer }}
28+
- name: Run Fuzzers (${{ matrix.sanitizer }})
29+
id: run
30+
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
31+
with:
32+
github-token: ${{ secrets.GITHUB_TOKEN }}
33+
fuzz-seconds: 300
34+
mode: "code-change"
35+
sanitizer: ${{ matrix.sanitizer }}
36+
output-sarif: true

0 commit comments

Comments
 (0)