Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ final class InMemoryDatasetStore: DatasetStore, Sendable {
for col in schema.columns {
try validateDatasetColumnIdentifier(col.name)
}
if let primaryKeyColumn = schema.primaryKeyColumn {
try validateDatasetColumnIdentifier(primaryKeyColumn)
}
for idx in indexes {
try validateDatasetColumnIdentifier(idx.column)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ final class PostgreSQLDatasetStore: DatasetStore, Sendable {
for col in schema.columns {
try validateDatasetColumnIdentifier(col.name)
}
if let primaryKeyColumn = schema.primaryKeyColumn {
try validateDatasetColumnIdentifier(primaryKeyColumn)
}
for idx in indexes {
try validateDatasetColumnIdentifier(idx.column)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ final class SQLiteDatasetStore: DatasetStore, Sendable {
for col in schema.columns {
try validateDatasetColumnIdentifier(col.name)
}
if let primaryKeyColumn = schema.primaryKeyColumn {
try validateDatasetColumnIdentifier(primaryKeyColumn)
}
for idx in indexes {
try validateDatasetColumnIdentifier(idx.column)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,22 @@ struct DatasetStoreTests {
}
}

/// SQL-injection attempt: double-quote in primary-key column name.
@Test func invalidIdentifier_primaryKeyColumn_sqlInjection_doubleQuote() async {
let ds = makeInMemory().datasetStore
let badName = "id\")); DROP TABLE dataset; --"
let schema = DatasetSchema(
columns: [
ColumnDeclaration(name: "id", type: .integer, nullable: false),
ColumnDeclaration(name: "value", type: .text, nullable: true)
],
primaryKeyColumn: badName
)
await #expect(throws: StorageError.self) {
try await ds.createDataset(id: UUID(), schema: schema, indexes: [])
}
}

/// Rejected column name in `appendRows` key must fail the whole
/// operation before any rows are written.
@Test func invalidIdentifier_inAppendRows() async throws {
Expand Down
3 changes: 3 additions & 0 deletions packages/kits/PersistenceKit/rust/src/dataset_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ impl DatasetStore for InMemoryDatasetStore {
for col in &schema.columns {
validate_dataset_column_identifier(&col.name)?;
}
if let Some(pk) = &schema.primary_key_column {
validate_dataset_column_identifier(pk)?;
}
for idx in indexes {
validate_dataset_column_identifier(&idx.column)?;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/kits/PersistenceKit/rust/src/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,9 @@ impl DatasetStore for PgDatasetStore {
for col in &schema.columns {
validate_dataset_column_identifier(&col.name)?;
}
if let Some(pk) = &schema.primary_key_column {
validate_dataset_column_identifier(pk)?;
}
for idx in indexes {
validate_dataset_column_identifier(&idx.column)?;
}
Expand Down
22 changes: 22 additions & 0 deletions packages/kits/PersistenceKit/rust/src/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,9 @@ impl DatasetStore for SqliteDatasetStoreShim {
for col in &schema.columns {
validate_dataset_column_identifier(&col.name)?;
}
if let Some(pk) = &schema.primary_key_column {
validate_dataset_column_identifier(pk)?;
}
for idx in indexes {
validate_dataset_column_identifier(&idx.column)?;
}
Expand Down Expand Up @@ -4140,6 +4143,25 @@ mod sql_identifier_injection_tests {
);
}

#[test]
fn create_dataset_rejects_primary_key_column_with_double_quote() {
let storage = injection_test_storage();
let ds = Storage::dataset_store(&storage).expect("dataset store");
let schema = DatasetSchema {
columns: vec![
ColumnDeclaration::new("id", ColumnType::Int),
ColumnDeclaration::new("value", ColumnType::Text),
],
primary_key_column: Some("id\")); DROP TABLE items; --".to_string()),
};

let err = ds.create_dataset(Uuid::new_v4(), &schema, &[]).unwrap_err();
assert!(
matches!(err, StorageError::InvalidIdentifier { .. }),
"expected InvalidIdentifier, got: {err:?}"
);
}

/// A semicolon in a column name allows statement stacking — classic
/// injection vector. Reject before the SQL string is built.
#[test]
Expand Down
Loading