diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9a6636b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,75 @@ +# Project guidance for Claude + +MemorySnapshotDataTool parses Unity memory snapshot (`.snap`) files and exports them to +DuckDB / SQLite databases, then runs SQL to build HTML reports. Because the whole tool is +built around composing and executing SQL, **SQL safety is a first-class rule in this repo.** + +## Rule: never build SQL from unsanitized external data + +External / untrusted data includes anything not hard-coded in source: CLI arguments, +file names and paths, environment values, fields read from a `.snap` file, values read +back out of the database, and anything derived from them. **Never** concatenate or string- +interpolate such data into a SQL statement. + +When you write or modify any code that builds a SQL string, follow these in order: + +1. **Bind values as parameters — always the default for any *value*.** + - **SQLite** (`Microsoft.Data.Sqlite`) — named parameters with a `$` prefix: + ```csharp + cmd.CommandText = "SELECT 1 FROM pragma_table_info($t) WHERE name = $c LIMIT 1"; + cmd.Parameters.AddWithValue("$t", tableName); + cmd.Parameters.AddWithValue("$c", columnName); + ``` + - **DuckDB** (`DuckDB.NET`) — positional `?` parameters in order: + ```csharp + cmd.CommandText = "... WHERE table_name = ? AND column_name = ? LIMIT 1"; + cmd.Parameters.Add(new DuckDBParameter { Value = tableName }); + cmd.Parameters.Add(new DuckDBParameter { Value = columnName }); + ``` + +2. **Identifiers (table / column names) can't be parameters in most positions.** + Validate them against a hard-coded safe-list, or query a catalog table that *does* + accept parameters (`information_schema.columns` for DuckDB, `pragma_table_info($t)` + for SQLite) instead of splicing the name into the statement. + +3. **Only interpolate a value directly if it is a non-string numeric type** (`int`, + `long`, …) that you control — a strongly-typed number cannot carry a payload. Add a + comment saying why it is safe, and prefer a parameter anyway when the API allows one. + +4. **Never** do `"... WHERE x = '" + value + "'"` or `$"... '{value}'"` with a string value. + +5. **Open read-only when a path only reads.** Report/analysis code that never writes opens the + database with least privilege — `Data Source=;Mode=ReadOnly` (SQLite) or + `Data Source=;ACCESS_MODE=READ_ONLY` (DuckDB) — so a bad query can't mutate data. Only + the export writers open read-write. + +## Canonical safe examples already in this repo + +Match these when you touch SQL — don't reinvent the pattern: + +- `Core/Report/Queries/SqliteReportQueries.cs` `HasColumn` — parameterized `pragma_table_info`. +- `Core/Report/Queries/DuckDbReportQueries.cs` `HasColumn` — identifier check via catalog table. +- `Core/Report/MultiSnapshotReport/MultiSnapshotReportBuilder.cs` `HasColumn` — both dialects parameterized. +- `Core/ExportDestination/DuckDbExportDestination.cs` — positional `?` parameters for inserts. +- `Core/ExportDestination/SqliteWriter.cs` — bulk inserts with `$pN` parameters. + +## The `ExecuteQuery(string sql)` contract + +`IReportQueryBackend.ExecuteQuery(string sql)` runs a raw SQL string and has **no parameter +overload**. It must therefore only ever receive an internally-constructed query — a constant +from `ReportSql` or one of its builders — never external input. The single dynamic builder, +`ReportSql.DownstreamStats(long rootIdx)`, interpolates a numeric `long`, which is injection- +safe. If you ever need to pass a *value* into a report query, add a parameterized path rather +than interpolating it into the SQL string. As defense-in-depth, the backends behind this sink +open the database read-only (rule 5 above), so a malformed query cannot mutate data. + +## Best-practices reference + +Full guidance for both humans and Claude — with rationale, anti-patterns, and review +checklist — is in [`docs/sql-safety.md`](docs/sql-safety.md). Read it before adding any new +query path or query builder. + +## Build & test + +- Build: `dotnet build MemorySnapshotDataTools.sln` +- Test: `dotnet test MemorySnapshotDataTools.sln` diff --git a/Core/Report/MultiSnapshotReport/MultiSnapshotReportBuilder.cs b/Core/Report/MultiSnapshotReport/MultiSnapshotReportBuilder.cs index 7e808f8..dbad14a 100644 --- a/Core/Report/MultiSnapshotReport/MultiSnapshotReportBuilder.cs +++ b/Core/Report/MultiSnapshotReport/MultiSnapshotReportBuilder.cs @@ -70,7 +70,8 @@ private static SnapshotMetricsRow QueryDatabase(string dbPath) private static SnapshotMetricsRow QueryDuckDb(string dbPath) { - using var connection = new DuckDBConnection($"Data Source={dbPath}"); + // Read-only: this path only queries metrics, never writes. See docs/sql-safety.md. + using var connection = new DuckDBConnection($"Data Source={dbPath};ACCESS_MODE=READ_ONLY"); connection.Open(); var snapshotMeta = QuerySnapshotMetadata(connection, isDuckDb: true); @@ -81,7 +82,8 @@ private static SnapshotMetricsRow QueryDuckDb(string dbPath) private static SnapshotMetricsRow QuerySqlite(string dbPath) { - using var connection = new SqliteConnection($"Data Source={dbPath}"); + // Read-only: this path only queries metrics, never writes. See docs/sql-safety.md. + using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); connection.Open(); var snapshotMeta = QuerySnapshotMetadata(connection, isDuckDb: false); @@ -112,12 +114,15 @@ private static Dictionary QueryNativeTypes(ob ? "COALESCE(SUM(resident_size_bytes), 0)" : nullResidentExpr; + // The resident expressions above are a closed set of hard-coded SQL fragments chosen by HasColumn; + // the native type name is a value, so it is bound as a parameter (DuckDB '?', SQLite '$nativeType'). + var nativeTypeParam = isDuckDb ? "?" : "$nativeType"; var assetBundleSql = $""" SELECT COUNT(*) AS obj_count, COALESCE(SUM(size_bytes), 0) AS allocated_bytes, {objectResidentExpr} AS resident_bytes FROM native_objects - WHERE native_type_name = '{GoldenValidationQueries.AssetBundleNativeTypeName}' + WHERE native_type_name = {nativeTypeParam} AND is_destroyed = false """; @@ -129,26 +134,32 @@ FROM native_roots WHERE {GoldenValidationQueries.SerializedFileAreaPredicate} """; - ReadNativeTypeAggregate(connection, isDuckDb, assetBundleSql, GoldenValidationQueries.AssetBundleNativeTypeName, result); + ReadNativeTypeAggregate(connection, isDuckDb, assetBundleSql, GoldenValidationQueries.AssetBundleNativeTypeName, result, + ("$nativeType", GoldenValidationQueries.AssetBundleNativeTypeName)); ReadNativeTypeAggregate(connection, isDuckDb, serializedFileSql, GoldenValidationQueries.SerializedFileMetricName, result); return result; } + // Table/column names are bound as parameters rather than interpolated, so this never builds SQL + // by concatenating identifiers. DuckDB queries information_schema.columns (a regular table that + // accepts bind parameters, matching DuckDbReportQueries.HasColumn); SQLite uses pragma_table_info + // with named parameters, matching SqliteReportQueries.HasColumn. private static bool HasColumn(object connection, bool isDuckDb, string tableName, string columnName) { - var sql = isDuckDb - ? $"SELECT 1 FROM pragma_table_info('{tableName}') WHERE name = '{columnName}' LIMIT 1" - : $"SELECT 1 FROM pragma_table_info('{tableName}') WHERE name = '{columnName}' LIMIT 1"; - if (isDuckDb) { using var cmd = ((DuckDBConnection)connection).CreateCommand(); - cmd.CommandText = sql; + cmd.CommandText = + "SELECT 1 FROM information_schema.columns WHERE table_schema = 'main' AND table_name = ? AND column_name = ? LIMIT 1"; + cmd.Parameters.Add(new DuckDBParameter { Value = tableName }); + cmd.Parameters.Add(new DuckDBParameter { Value = columnName }); return cmd.ExecuteScalar() != null; } using var sqliteCmd = ((SqliteConnection)connection).CreateCommand(); - sqliteCmd.CommandText = sql; + sqliteCmd.CommandText = "SELECT 1 FROM pragma_table_info($t) WHERE name = $c LIMIT 1"; + sqliteCmd.Parameters.AddWithValue("$t", tableName); + sqliteCmd.Parameters.AddWithValue("$c", columnName); return sqliteCmd.ExecuteScalar() != null; } @@ -157,12 +168,16 @@ private static void ReadNativeTypeAggregate( bool isDuckDb, string sql, string typeName, - Dictionary result) + Dictionary result, + params (string Name, object Value)[] parameters) { if (isDuckDb) { using var cmd = ((DuckDBConnection)connection).CreateCommand(); cmd.CommandText = sql; + // DuckDB binds positionally ('?'), in declaration order. + foreach (var (_, value) in parameters) + cmd.Parameters.Add(new DuckDBParameter { Value = value }); using var reader = cmd.ExecuteReader(); if (!reader.Read()) return; @@ -179,6 +194,8 @@ private static void ReadNativeTypeAggregate( using var sqliteCmd = ((SqliteConnection)connection).CreateCommand(); sqliteCmd.CommandText = sql; + foreach (var (name, value) in parameters) + sqliteCmd.Parameters.AddWithValue(name, value); using var sqliteReader = sqliteCmd.ExecuteReader(); if (!sqliteReader.Read()) return; diff --git a/Core/Report/Queries/DuckDbReportQueries.cs b/Core/Report/Queries/DuckDbReportQueries.cs index 80651da..045fb97 100644 --- a/Core/Report/Queries/DuckDbReportQueries.cs +++ b/Core/Report/Queries/DuckDbReportQueries.cs @@ -7,11 +7,13 @@ internal sealed class DuckDbReportQueries : IReportQueryBackend { private readonly DuckDBConnection _connection; - /// Opens a connection to the DuckDB database at the given path. + /// Opens a read-only connection to the DuckDB database at the given path. /// Path to the .duckdb file. public DuckDbReportQueries(string dbPath) { - _connection = new DuckDBConnection($"Data Source={dbPath}"); + // The report path only ever runs SELECTs. Open read-only (least privilege) so that even a + // malformed query reaching ExecuteQuery cannot modify or drop data. See docs/sql-safety.md. + _connection = new DuckDBConnection($"Data Source={dbPath};ACCESS_MODE=READ_ONLY"); _connection.Open(); } @@ -43,9 +45,14 @@ public bool HasColumn(string tableName, string columnName) { try { - var (_, rows) = ExecuteQuery( - $"SELECT 1 FROM information_schema.columns WHERE table_schema = 'main' AND table_name = '{tableName.Replace("'", "''")}' AND column_name = '{columnName.Replace("'", "''")}' LIMIT 1"); - return rows.Count > 0; + // Bind table/column names as parameters rather than interpolating them. information_schema.columns + // is a regular table, so it accepts bind parameters (DuckDB uses positional '?'). + using var cmd = _connection.CreateCommand(); + cmd.CommandText = + "SELECT 1 FROM information_schema.columns WHERE table_schema = 'main' AND table_name = ? AND column_name = ? LIMIT 1"; + cmd.Parameters.Add(new DuckDBParameter { Value = tableName }); + cmd.Parameters.Add(new DuckDBParameter { Value = columnName }); + return cmd.ExecuteScalar() != null; } catch { diff --git a/Core/Report/Queries/IReportQueryBackend.cs b/Core/Report/Queries/IReportQueryBackend.cs index 6fafafc..5a2184c 100644 --- a/Core/Report/Queries/IReportQueryBackend.cs +++ b/Core/Report/Queries/IReportQueryBackend.cs @@ -20,7 +20,13 @@ internal interface IReportQueryBackend : IDisposable ReportBackendDialect Dialect { get; } /// Executes the given SQL and returns column names and rows (null for missing values). - /// SQL query (single statement). + /// + /// SQL query (single statement). Must be an internally-constructed query — a constant from + /// or one of its builders — never external/untrusted input. The only + /// dynamic value, , interpolates a numeric long + /// index, which cannot carry a SQL-injection payload. As defense-in-depth, implementations open + /// the database read-only, so a malformed query here cannot modify or drop data. + /// /// Column names and list of row arrays. (string[] Columns, List Rows) ExecuteQuery(string sql); diff --git a/Core/Report/Queries/SqliteReportQueries.cs b/Core/Report/Queries/SqliteReportQueries.cs index d3b8240..581d6c6 100644 --- a/Core/Report/Queries/SqliteReportQueries.cs +++ b/Core/Report/Queries/SqliteReportQueries.cs @@ -7,11 +7,13 @@ internal sealed class SqliteReportQueries : IReportQueryBackend { private readonly SqliteConnection _connection; - /// Opens a connection to the SQLite database at the given path. + /// Opens a read-only connection to the SQLite database at the given path. /// Path to the .db or .sqlite file. public SqliteReportQueries(string dbPath) { - _connection = new SqliteConnection($"Data Source={dbPath}"); + // The report path only ever runs SELECTs. Open read-only (least privilege) so that even a + // malformed query reaching ExecuteQuery cannot modify or drop data. See docs/sql-safety.md. + _connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); _connection.Open(); } diff --git a/Core/Report/SummaryReportRunner.cs b/Core/Report/SummaryReportRunner.cs index 772c552..c3b8d54 100644 --- a/Core/Report/SummaryReportRunner.cs +++ b/Core/Report/SummaryReportRunner.cs @@ -135,9 +135,10 @@ private static SummaryReport FromSnapshot(string snapshotPath, IProgressReporter /// private static SummaryReport FromDatabase(string databasePath, string extension) { + // Read-only: the summary path only reads from the database. See docs/sql-safety.md. using DbConnection connection = extension.Equals(".db", StringComparison.OrdinalIgnoreCase) - ? new SqliteConnection($"Data Source={databasePath}") - : new DuckDBConnection($"Data Source={databasePath}"); + ? new SqliteConnection($"Data Source={databasePath};Mode=ReadOnly") + : new DuckDBConnection($"Data Source={databasePath};ACCESS_MODE=READ_ONLY"); connection.Open(); var info = ReadSnapshotInfo(connection); diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6a0a1aa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security + +## Reporting a vulnerability + +Report suspected security issues to **zack.asofsky@unity3d.com**. Please do not open public issues for undisclosed vulnerabilities. + +## SQL safety + +This tool composes and executes SQL against DuckDB and SQLite. Safe-query rules and best +practices are documented in [`docs/sql-safety.md`](docs/sql-safety.md), with the enforced rule +for contributors (and Claude) in [`CLAUDE.md`](CLAUDE.md). In short: bind values as parameters, +never interpolate external data, validate identifiers against a safe-list or parameterized +catalog lookup, and open read-only on any path that only reads. + +## SAST finding triage log + +Findings from static analysis (Cycode SAST) that have been reviewed and accepted are recorded +here so the rationale is durable and reviewable. Suppression itself is performed in Cycode, not +in this repo — see "How we suppress" below. + +### CYCODE-SAST: Unsanitized external input in SQL query — `ExecuteQuery` + +| Field | Value | +| --- | --- | +| Scanner | Cycode SAST | +| Rule | Unsanitized external input in SQL query (SQL injection) | +| Location | [`Core/Report/Queries/SqliteReportQueries.cs`](Core/Report/Queries/SqliteReportQueries.cs) — `ExecuteQuery(string sql)` (and the equivalent `DuckDbReportQueries.ExecuteQuery`) | +| Reviewed | 2026-06-01, Zack Asofsky | +| Disposition | **False positive** — no external/untrusted input reaches the sink — accepted with defense-in-depth mitigations | + +**Rationale.** `ExecuteQuery(string sql)` only ever receives internally-constructed queries: +compile-time constants from `ReportSql` and its builders. The sole dynamic value, +`ReportSql.DownstreamStats(long rootIdx)`, interpolates a numeric `long`, which cannot carry an +injection payload. No CLI argument, file path, or snapshot field reaches the query string. + +**Defense-in-depth mitigations applied** (branch `bugfix/sql-sanitization`): + +- Report/analysis database connections are opened **read-only** (`Mode=ReadOnly` for SQLite, + `ACCESS_MODE=READ_ONLY` for DuckDB), so a malformed query reaching this sink cannot modify or + drop data. Only the export writers open read-write. +- Identifier-bearing helper queries (`HasColumn`) use parameterized catalog lookups + (`pragma_table_info($t)` / `information_schema.columns` with bind parameters) instead of + string concatenation. +- The multi-snapshot native-type filter binds its value as a parameter rather than interpolating + it. +- The `ExecuteQuery` contract is documented on `IReportQueryBackend`, and the rules are codified + in [`docs/sql-safety.md`](docs/sql-safety.md) and [`CLAUDE.md`](CLAUDE.md). \ No newline at end of file diff --git a/docs/sql-safety.md b/docs/sql-safety.md new file mode 100644 index 0000000..ff54677 --- /dev/null +++ b/docs/sql-safety.md @@ -0,0 +1,176 @@ +# SQL safety: avoiding SQL injection + +This tool is built around composing and executing SQL against DuckDB and SQLite databases. +This page is the canonical reference for writing query code safely. It is written for both +human contributors and for Claude (the project rule lives in +[`CLAUDE.md`](https://github.com/Unity-Technologies/cse-memory-snapshot-data-tool/blob/main/CLAUDE.md) +and points here). + +## The one rule + +**Never build a SQL statement by concatenating or interpolating external data.** Bind data +as parameters instead. + +"External data" is anything not hard-coded in source: + +- CLI arguments (directory, filter, title, output paths, …) +- File names and filesystem paths +- Environment variables +- Fields parsed out of a `.snap` snapshot file (type names, object names, area names, …) +- Values read back out of the database +- Anything computed or derived from any of the above + +Even when a value happens to be a compile-time constant today, prefer the safe pattern so +the code stays safe when someone later swaps the constant for a variable. + +## Why it matters + +When external text is spliced straight into SQL, a value such as +`'; DROP TABLE native_objects;--` is parsed as *code* rather than *data*. Parameter binding +keeps the SQL logic and the data on separate channels, so the database engine always treats +bound values as literals — never as SQL. This is the prevention technique recommended by the +[OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html). + +## Patterns by category + +### 1. Values → always parameterize + +This is the default for every value (numbers, strings, blobs, dates). The two database +libraries used in this repo bind parameters differently. + +**SQLite — `Microsoft.Data.Sqlite`** uses **named** parameters with a `$` prefix: + +```csharp +using var cmd = connection.CreateCommand(); +cmd.CommandText = "SELECT COUNT(*) FROM native_objects WHERE native_type_name = $type"; +cmd.Parameters.AddWithValue("$type", typeName); +``` + +**DuckDB — `DuckDB.NET`** uses **positional** `?` parameters, bound in order: + +```csharp +using var cmd = connection.CreateCommand(); +cmd.CommandText = "SELECT COUNT(*) FROM native_objects WHERE native_type_name = ?"; +cmd.Parameters.Add(new DuckDBParameter { Value = typeName }); +``` + +For bulk inserts, generate numbered placeholders and bind each value — see +`SqliteWriter.CreateBulkInsertCommand` (`$p0`, `$p1`, …) and the `native_objects`/ +`managed_objects` writers, and the positional inserts in `DuckDbExportDestination`. + +### 2. Identifiers (table / column names) → safe-list or catalog lookup + +A parameter placeholder cannot stand in for a table or column name in most SQL positions, +so identifiers need a different defense: + +- **Safe-list:** compare against a hard-coded set of known-good identifiers and reject + anything else. In this repo, table and column names are themselves constants, which is the + simplest form of a safe-list. +- **Catalog lookup with parameters:** to check whether a column exists, query a catalog + table/function that *does* accept a bound parameter rather than splicing the name in. + +```csharp +// SQLite — pragma_table_info accepts a bound parameter: +sqliteCmd.CommandText = "SELECT 1 FROM pragma_table_info($t) WHERE name = $c LIMIT 1"; +sqliteCmd.Parameters.AddWithValue("$t", tableName); +sqliteCmd.Parameters.AddWithValue("$c", columnName); + +// DuckDB — information_schema.columns is a regular table, so it accepts parameters: +cmd.CommandText = + "SELECT 1 FROM information_schema.columns " + + "WHERE table_schema = 'main' AND table_name = ? AND column_name = ? LIMIT 1"; +cmd.Parameters.Add(new DuckDBParameter { Value = tableName }); +cmd.Parameters.Add(new DuckDBParameter { Value = columnName }); +``` + +If you ever must place an identifier literally and parameters genuinely can't reach the +position, validate it against a safe-list first; only as a last resort escape it (e.g. +doubling single quotes for a quoted string argument), and document why. + +### 3. Numeric values you control → direct interpolation is acceptable + +A strongly-typed number cannot carry an injection payload, so interpolating a `long`/`int` +that you produced (for example, an index read from your own query result) is acceptable when +no parameter API is available. Keep the variable strongly typed — never widen it to `string` +— and leave a comment explaining the guarantee. + +```csharp +// rootIdx is a long sourced from our own query result; a numeric literal can't inject. +var sql = $"... WHERE c.from_index = {rootIdx} ..."; +``` + +This is exactly how `ReportSql.DownstreamStats(long rootIdx)` works. + +### 4. SQL fragments assembled from a fixed set → not data, keep them constant + +Some queries are assembled from alternative SQL *expressions* chosen at runtime (e.g. a +resident-size expression that differs by dialect, or a `COALESCE`→`IFNULL` rewrite for +SQLite). These fragments are hard-coded source, not external data, so composing them is fine +— but the inputs that decide *which* fragment to use must come from a closed set you control, +never from external text. + +## The `ExecuteQuery(string sql)` contract + +`IReportQueryBackend.ExecuteQuery(string sql)` executes a raw SQL string and has **no +parameter overload**. By contract it must receive only internally-constructed SQL — a +constant from `ReportSql` or one of its builders — and **never** external input. The only +dynamic builder, `ReportSql.DownstreamStats`, interpolates a numeric `long` (safe per +category 3). + +If a future report query needs a real *value* from outside, do **not** interpolate it into +the string passed to `ExecuteQuery`. Add a parameterized execution path instead. + +## Least privilege: open read paths read-only + +`ExecuteQuery(string sql)` runs a whole query string, so it cannot be parameterized away — the +defense for that sink is least privilege. Code paths that only read (the report query backends, +the multi-snapshot builder, the summary runner) open the database **read-only**, so even a +malformed or unexpected query cannot modify or drop data: + +- **SQLite:** `Data Source=;Mode=ReadOnly` +- **DuckDB:** `Data Source=;ACCESS_MODE=READ_ONLY` + +Only the export writers (`DuckDbExportDestination`, `SqliteWriter`), which create the database, +open read-write. When you add a new code path that only reads from a database, open it read-only. + +## Anti-patterns — never do these + +```csharp +// ❌ String value concatenated into SQL +var sql = "SELECT * FROM native_objects WHERE name = '" + name + "'"; + +// ❌ String value interpolated into SQL +var sql = $"SELECT * FROM native_objects WHERE name = '{name}'"; + +// ❌ Identifier interpolated with no safe-list / escaping +var sql = $"SELECT 1 FROM pragma_table_info('{tableName}')"; + +// ❌ Passing externally-influenced text to ExecuteQuery(string sql) +backend.ExecuteQuery("SELECT * FROM " + userSuppliedTable); +``` + +## Review checklist + +Before merging code that touches SQL, confirm: + +- [ ] Every **value** in the statement is a bound parameter (`$name` for SQLite, + `?` for DuckDB) — not concatenated or interpolated. +- [ ] Every **identifier** is a hard-coded constant, validated against a safe-list, or + resolved through a parameterized catalog query. +- [ ] Any directly-interpolated value is a strongly-typed number you control, with a comment + explaining why it's safe. +- [ ] Nothing passed to `ExecuteQuery(string sql)` is, or is derived from, external input. +- [ ] New code matches an existing safe example rather than introducing a new pattern. + +## Canonical safe examples in this codebase + +- `Core/Report/Queries/SqliteReportQueries.cs` — parameterized `pragma_table_info`. +- `Core/Report/Queries/DuckDbReportQueries.cs` — identifier existence check via catalog table. +- `Core/Report/MultiSnapshotReport/MultiSnapshotReportBuilder.cs` — `HasColumn` parameterized for both dialects. +- `Core/ExportDestination/DuckDbExportDestination.cs` — positional `?` parameter inserts. +- `Core/ExportDestination/SqliteWriter.cs` — bulk inserts with `$pN` parameters. + +## References + +- [OWASP — SQL Injection](https://owasp.org/www-community/attacks/SQL_Injection) +- [OWASP — SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html) diff --git a/mkdocs.yml b/mkdocs.yml index 3d8d679..3b99230 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ nav: - Introduction to cse-memory-snapshot-data-tool: "intro.md" - Architecture and design: "design.md" - Snap File Format: "snap-file-format.md" + - SQL safety: "sql-safety.md" - Installing for local development: "installation.md" - Troubleshooting: "troubleshooting.md" - Runbook: "runbook.md"