Skip to content

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

Open
HandSonic wants to merge 4 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 4 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.

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