Skip to content

fix(h2): escape SQL identifiers and literals in metadata/DDL paths (#1914)#2177

Open
HandSonic wants to merge 5 commits into
OtterMind:mainfrom
HandSonic:fix/sqli-h2
Open

fix(h2): escape SQL identifiers and literals in metadata/DDL paths (#1914)#2177
HandSonic wants to merge 5 commits into
OtterMind:mainfrom
HandSonic:fix/sqli-h2

Conversation

@HandSonic

@HandSonic HandSonic commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Related issue

Part of the SQL-injection hardening tracked in #1914 (multi-plugin effort; this PR covers H2). Prior art: #2052 (Oracle escapeSqlLiteral), #2053 (SQL Server identifier quoting).

Summary

These are second-order injection paths: values such as table/schema/view/index names originate from the connected database's own metadata, so exploitation requires a maliciously named object in a target database. The fix is still worthwhile hardening and matches the pattern already merged for Oracle and SQL Server.

What changed:

  • New H2SqlEscapes helper:
    • escapeSqlLiteral — neutralizes values interpolated into single-quoted SQL string literals (single-quote doubling).
    • quoteIdentifier/escapeIdentifier — strips one surrounding quote pair and doubles every embedded double quote (") for quoted-identifier positions.
  • Applied across metadata/DDL SQL-building sites in 7 files (see diff).
  • Note: generated DDL in tableDDL now double-quotes identifiers; quoted output is more faithful to actual object names (previously identifiers were emitted unquoted). The assertion in H2MetaResultSetLifecycleTest was updated accordingly — it now expects the quoted form rather than a weakened check.
  • Added H2SqlEscapesTest covering literal doubling, identifier doubling, and malicious-name neutralization.

Affected surfaces

  • Frontend / Web
  • Backend / API / Storage
  • Database plugin / Driver
  • JCEF / Desktop packaging
  • CI / Build / Release
  • Documentation only

Verification

  • Command: mvn -B -pl chat2db-community-plugins/chat2db-community-h2 -f chat2db-community-server/pom.xml -Dmaven.test.skip=false -DskipTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.test.failure.ignore=false clean test
  • Result: Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 (includes pre-existing module tests).
  • Manual verification: escaping is identity for names without special characters; a name containing " is emitted with doubled quotes inside a delimited identifier.

Risk and compatibility

  • Public API or stored data: N/A — only changes generated SQL for metadata/DDL paths.
  • Database or driver compatibility: quoted output matches H2 double-quote identifier rules; behavior for ordinary names is unchanged apart from identifiers in generated DDL now being delimited (which H2 accepts and which matches the actual object names more faithfully).
  • Network, privacy, or security: closes second-order SQL-injection paths in generated metadata/DDL SQL.
  • Community / Local / Pro boundary: N/A.
  • Backward compatibility: names containing quote characters that previously produced broken/injectable SQL now produce correctly escaped SQL.

Reviewer map

  • Start here: H2SqlEscapes.java (new helper) and H2SqlEscapesTest.java.
  • Then: quoting call sites in H2MetaData / H2DBManager / H2SqlBuilder; the updated assertion in H2MetaResultSetLifecycleTest.
  • Failure condition: a metadata/DDL SQL string still interpolates an unescaped name or literal.
  • Rollback or disable path: revert the commit on this branch.

Contributor declaration

  • I linked the Issue that defines this change.
  • I tested the affected behavior and reported the actual results above.
  • I did not include credentials, private data, or generated build output.
  • I disclosed substantial AI assistance below, or this PR contains no substantial AI-generated code.

AI assistance: The fix, verification, and PR description were produced with AI coding assistance.

…tterMind#1914)

Override quoteIdentifier/quoteQualifiedIdentifier/buildTableName/buildColumns
and reimplement buildCreateTable/buildAlterTable/buildUpdate/buildTemplate/
buildCreateDatabase in H2SqlBuilder so every identifier is double-quote
escaped and every comment literal is single-quote escaped. Validate
metadata TYPE_NAME against an allow-list and neutralize hostile COLUMN_DEF
defaults in generated DDL. Apply the export SCRIPT NODATA sentinel before
substituting the escaped schema name so schema names containing NODATA are
not corrupted. Document the raw-name contract on H2SqlEscapes and add
attack-string tests for the builder, manager and metadata paths.
…tterMind#1914)

Route column.getColumnType() in buildCreateTable and generateAlterColumnSql
(ADD/MODIFY) through H2SqlEscapes.requireSafeTypeName so a hostile type string
cannot break out of generated DDL. Adds attack-string rejection tests.

@openai0229 openai0229 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for hardening these second-order SQL injection paths. The escaping behavior added here is needed, but H2SqlEscapes creates a second dialect-escaping abstraction. The Community SPI already defines ISQLIdentifierProcessor as the owner of both identifier quoting (quoteIdentifier) and SQL string-literal escaping (escapeString). H2 currently inherits DefaultSQLIdentifierProcessor through DefaultMetaService.

Please implement an H2-specific identifier processor (or strengthen the appropriate shared processor), return it from H2Meta#getSQLIdentifierProcessor(), and reuse that processor from H2Meta, H2DBManager, and H2SqlBuilder instead of routing these call sites through H2SqlEscapes.

Simply switching to the current DefaultSQLIdentifierProcessor is not sufficient: its quoteIdentifier only wraps invalid names and does not double embedded double quotes, while its escapeString preserves adjacent quote pairs instead of encoding every quote in a raw value. The processor implementation therefore needs focused coverage for embedded identifier quotes, already quoted identifiers, consecutive literal quotes, and case-sensitive H2 metadata names. Identifier quoting and string-literal escaping should remain separate processor methods.

This keeps one reusable dialect contract and prevents future H2 SQL-building call sites from bypassing the hardening.

@openai0229

openai0229 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Would you be willing to take this opportunity to improve this class of architectural issue more broadly, rather than limiting the change to the seven H2 call sites in this diff?

It looks likely that identifier quoting and SQL string-literal escaping have drifted away from ISQLIdentifierProcessor in multiple places. If you are open to it, could you audit the relevant plugin SQL-construction paths and refactor the non-conforming logic to use the processor abstraction consistently, strengthening the processor implementations where needed? The goal would be to eliminate parallel per-plugin escapers and make the existing architecture the single reusable path.

This is a broader improvement request, so please let us know whether you would be willing to take it on as part of this work, or as focused follow-up work if the scope is too large for this PR.

… review (OtterMind#1914)

- new H2IdentifierProcessor (SPI ISQLIdentifierProcessor): quoteIdentifier
  with double-quote doubling, escapeString with single-quote doubling
- H2Meta overrides getSQLIdentifierProcessor(); metadata call sites use it
- builders/managers use H2IdentifierProcessor.INSTANCE
- non-escapable validation moved to H2SqlGuards (type names, column defaults)
- H2SqlEscapes removed; tests migrated (27 green)
@HandSonic

Copy link
Copy Markdown
Contributor Author

Agreed — thank you for the direction. I've reworked this branch onto the SPI contract:

  • New H2IdentifierProcessor extends DefaultSQLIdentifierProcessor: quoteIdentifier (double-quote doubling, strips one outer pair) and escapeString (single-quote doubling)
  • H2Meta now overrides getSQLIdentifierProcessor(); all metadata call sites use it
  • Builder/manager call sites use H2IdentifierProcessor.INSTANCE
  • Non-escapable validation (metadata-reported type names, column default expressions) moved to H2SqlGuards, clearly separated from escaping
  • H2SqlEscapes removed; tests migrated (27 green)

I will roll the same pattern out to the sibling PRs (#2172-#2176 and #2194-#2206): each plugin gets its escaping consolidated into its own ISQLIdentifierProcessor implementation (strengthening the existing processor where one already exists, e.g. mysql/oracle/sqlserver), with getSQLIdentifierProcessor() overridden on the MetaData side. The broader audit of escaping drift across plugins can follow as a separate pass once these branches share the same shape.

@HandSonic

Copy link
Copy Markdown
Contributor Author

Rollout complete: all 19 sibling PRs (#2172-#2176, #2194-#2206) now share the same shape as this branch — escaping consolidated into each plugin's ISQLIdentifierProcessor implementation (strengthened existing processors where present), getSQLIdentifierProcessor() overridden on the MetaData side, non-escapable validation in per-plugin SqlGuards.

One extra finding from our own adversarial pass during the rollout, worth flagging: making quoteIdentifier unconditionally quote broke SPI consumers (GenericSqlCompletionEngine relies on conditional-quote semantics + null passthrough). The final contract is therefore two-track: quoteIdentifier stays conditional (null->null, valid plain identifiers unquoted, fold-aware for PG) and a plugin-level quoteIdentifierAlways serves DDL-generation sites. Backtick processors override removeIdentifierQuote/isQuoteIdentifier accordingly. All module test suites green (incl. mysql 631 tests).

@openai0229

Copy link
Copy Markdown
Contributor

Thanks for completing the rollout. I reviewed the current heads of all 19 PRs. Moving the escaping implementations into plugin processors is the right direction, but the rollout is not merge-ready yet.

The main common blocker is that identifier quote/remove round-trip is still broken across the 18 relational branches. For example, a raw identifier such as A"B is quoted as "A""B", but removeIdentifierQuote returns AB instead of A"B. The backtick and SQL Server bracket implementations have the same issue. This is observable through GenericSqlCompletionEngine, which removes quotes before metadata lookup.

There are also contract and dialect regressions in the current heads:

  • quoteIdentifierAlways remains a concrete plugin API rather than part of ISQLIdentifierProcessor, while several production paths still use content-only escapeIdentifier helpers plus caller-owned delimiters.
  • H2 and XuguDB do not implement the stated conditional/null-preserving/always-quote split.
  • KingBase, DM, Oscar, Redshift, Snowflake, and OceanBase Oracle regress existing case-folding or reserved-word behavior.
  • Several SqlGuards either accept structural SQL such as added constraints/columns or convert legal default expressions into string literals.
  • MongoDB correctly needs a non-SQL path, but its current allowlist accepts names such as 1users and my-field that produce invalid JavaScript in db.%s and unquoted object-key positions.

Please add a shared processor conformance test class and run every relational processor through it. At minimum it should cover:

  • null and blank passthrough;
  • conditional quote versus always-quote semantics;
  • dialect reserved words and case folding;
  • removeIdentifierQuote(quoteIdentifierAlways(raw)) == raw for embedded ", backtick, and ] delimiters;
  • already quoted input normalization;
  • raw string escaping, including consecutive quote characters and null;
  • an integration case through the shared completion/metadata consumer.

Please also add plugin-level SQL/DDL semantic tests for legal default expressions and structural breakout inputs. Where an embedded or test-container database is available, execute the generated DDL rather than only asserting string fragments. SQLite should prove that a type such as TEXT, injected INTEGER cannot create another column; H2/PostgreSQL/Oracle-family tests should prove that legal function or typed defaults retain their original semantics. MongoDB should use syntax-aware command tests or a structured builder so valid database, collection, and field names are not conflated.

The shared SPI contract and conformance suite should be fixed first, then the sibling branches should be rebased onto it. Otherwise the same contract bug has to be corrected independently in every PR.

…h, conditional for SPI, always for DDL) (OtterMind#1914)

The pilot branch predated the dual-track contract; its always-quote
processor had the same null->"" garbage and completion regression the
other modules were fixed for.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

2 participants