posthog/red-team
SQL injection and input validation audit for the DuckHog SQL proxy pipeline. Systematically tests all paths where user-supplied identifiers or values flow into rewritten SQL strings. Use as a routine security check after modifying any rewriter, serializer, or SQL-generation code.
npx skills add https://github.com/PostHog/duckhog --skill red-team
DuckHog rewrites user SQL and forwards it to a remote database over Arrow Flight.
Every rewriter is a potential injection surface. This skill systematically probes
all paths where user input flows into generated SQL strings.
All user-supplied identifiers MUST pass through KeywordHelper::WriteOptionallyQuoted()
and all values through Value::ToSQLString(). Any path that performs raw string
concatenation with user input is a vulnerability.
| Surface | Entry point | What to probe |
|---------|------------|---------------|
| INSERT serialization | posthog_sql_utils.cpp:BuildInsertSQL | Value escaping: ', '', \, NULL bytes, overlong UTF-8, nested quotes in LIST/STRUCT |
| DELETE rewriter | posthog_dml_rewriter.cpp | Table/schema names, WHERE clause expressions, USING clause refs |
| UPDATE rewriter | posthog_dml_rewriter.cpp | SET expressions, FROM clause refs, subqueries |
| MERGE rewriter | posthog_dml_rewriter.cpp | Source table refs, ON clause, WHEN expressions |
| View rewriter | posthog_view_rewriter.cpp | View name, schema, column aliases, query body |
| CTAS rewriter | posthog_ctas_rewriter.cpp | Table name, column names, type names |
| Rename table | posthog_schema_entry.cpp | Old/new table names |
| Partition alter | posthog_schema_entry.cpp | Partition column names |
| Time travel | RenderAtClauseSQL | VERSION and TIMESTAMP values |
| ATTACH URI | posthog_catalog.cpp | flight_server, user, password connection params |
| Arrow Flight results | arrow_stream.cpp:Produce | Column names in projection, schema/table in FROM |
For each surface, read the source and identify every point where a string is
built from user-supplied input. Classify each as:
WriteOptionallyQuoted() or ToSQLString() or DuckDB's AST serializerStringUtil::Format, or manual quotingRecord findings in a table:
| File:Line | Input source | Escaping method | Classification |
For each suspect or vulnerable path, create SQLLogicTest cases. Use payloads from
references/payloads.md.
Test structure:
# RED-TEAM: [surface] — [payload category]
# Expected: statement error OR correct escaped output
# Vulnerable if: injected SQL executes, data leaks, or server crashes
statement ok
CREATE TABLE remote_flight.schema."Robert'; DROP TABLE t--"(id INT);
statement ok
INSERT INTO remote_flight.schema."Robert'; DROP TABLE t--" VALUES (1);
query I
SELECT id FROM remote_flight.schema."Robert'; DROP TABLE t--";
----
1
statement ok
DROP TABLE remote_flight.schema."Robert'; DROP TABLE t--";
# Unit tests (C++ rewriter tests)
./build/release/test/unittest "[duckhog],test/sql/unit/*"
# Integration tests (full pipeline)
./scripts/test-servers.sh start --background --seed
eval "$(./scripts/test-servers.sh env)"
./build/release/test/unittest "test/sql/integration/*"
./scripts/test-servers.sh stop
For each test case:
| Result | Classification | Action |
|--------|---------------|--------|
| statement error with expected message | Safe — injection rejected | No action |
| Correct output with escaped identifiers | Safe — properly quoted | No action |
| Injected SQL executes | Vulnerability | File issue, fix immediately |
| Server crash / segfault | Vulnerability | File issue, fix immediately |
| Unexpected error message | Investigate | May be safe but fragile |
Output a summary table:
| Surface | Vectors tested | Safe | Suspect | Vulnerable |
For each vulnerability found, provide:
After fixing a vulnerability, add the adversarial test case to the permanent
test suite under test/sql/unit/security/ or test/sql/integration/security/.
These tests should never be removed.
references/payloads.mdreferences/surface-map.mdTake posthog/red-team from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.