Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cc70b7b
fix(report): safely enable query by example
Ben-Heerema Aug 6, 2026
4477e8e
fix(report): address query review findings
Ben-Heerema Aug 6, 2026
ee4a8db
fix(report): harden schema comparison
Ben-Heerema Aug 6, 2026
233df2b
chore(report): document scanner boundaries
Ben-Heerema Aug 6, 2026
d37a0bf
test(report): complete query review follow-ups
Ben-Heerema Aug 6, 2026
5962421
fix(report): bound query by example execution
Ben-Heerema Aug 6, 2026
3f20ffb
fix(report): close query review edge cases
Ben-Heerema Aug 6, 2026
3324c57
fix(report): advertise favorite post method
Ben-Heerema Aug 6, 2026
5ebfa98
fix(report): secure query favorites
Ben-Heerema Aug 6, 2026
a6e0988
fix(report): harden favorite form markup
Ben-Heerema Aug 6, 2026
2d4ad22
test(report): include query tests in CI
Ben-Heerema Aug 7, 2026
2f99beb
fix(report): address live quality findings
Ben-Heerema Aug 7, 2026
b733a74
fix(report): clear remaining quality findings
Ben-Heerema Aug 7, 2026
85f88f2
fix(report): preserve query cleanup context
Ben-Heerema Aug 7, 2026
ef1f928
refactor(db): simplify quoted SQL scanning
Ben-Heerema Aug 7, 2026
1146b3f
fix(report): address CI review findings
Ben-Heerema Aug 7, 2026
4f210fa
fix(report): prevent query-by-example secret access
Ben-Heerema Aug 7, 2026
1cda36b
test(report): verify disabled query is not executed
Ben-Heerema Aug 7, 2026
d7671c7
refactor(report): address static review findings
Ben-Heerema Aug 7, 2026
41e6d5d
test(report): prevent sensitive table policy drift
Ben-Heerema Aug 7, 2026
a15394e
fix(report): render numeric query results
Ben-Heerema Aug 7, 2026
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
8 changes: 8 additions & 0 deletions dependencies-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@
<owasp-encoder.version>1.4.0</owasp-encoder.version>
<!-- OWASP CSRFGuard -->
<csrfguard.version>4.5.0-jakarta</csrfguard.version>
<!-- Structural validation for authorized Query-by-Example SELECTs -->
<jsqlparser.version>5.3</jsqlparser.version>
<!-- JNA: pin transitive ultrabuk-htmltopdf-java range [5.12,) to the locked version -->
<jna.version>5.19.0</jna.version>
<!-- HAPI HL7 v2 -->
Expand Down Expand Up @@ -445,6 +447,20 @@
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>

<!-- SQL parsing used to fail closed before Query-by-Example reaches JDBC -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
<exclusions>
<!-- Benchmarking support is not used by the parser at runtime. -->
<exclusion>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- ==================== HTTP Client ==================== -->

<!-- Apache HttpClient 5.x — migrated from 4.5.14 (httpclient + httpmime merged into httpclient5) -->
Expand Down Expand Up @@ -1446,6 +1462,13 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.14</version>
<configuration>
<!-- JSqlParser's generated token-manager method exceeds the JVM method
size after instrumentation; dependency code is outside our coverage scope. -->
<excludes>
<exclude>net.sf.jsqlparser.*</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,15 @@
import io.github.carlos_emr.carlos.commn.model.ReportByExamplesFavorite;

public interface ReportByExamplesFavoriteDao extends AbstractDao<ReportByExamplesFavorite> {
List<ReportByExamplesFavorite> findByQuery(String query);
/**
* Finds favorites owned by one provider whose saved SQL exactly matches the supplied query.
*
* @param providerNo owner provider number
* @param query exact saved query text
* @return matching favorites owned by the provider, or an empty list when none exist
* @since 2026-08-06
*/
List<ReportByExamplesFavorite> findByProviderAndQuery(String providerNo, String query);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

List<ReportByExamplesFavorite> findByEverything(String providerNo, String favoriteName, String queryString);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ public ReportByExamplesFavoriteDaoImpl() {
}

@Override
public List<ReportByExamplesFavorite> findByQuery(String query) {
Query q = createQuery("ex", "ex.query LIKE ?1");
q.setParameter(1, query);
return q.getResultList();
public List<ReportByExamplesFavorite> findByProviderAndQuery(String providerNo, String queryString) {
Query query = createQuery("ex", "ex.providerNo = ?1 AND ex.query = ?2");
query.setParameter(1, providerNo);
query.setParameter(2, queryString);
return query.getResultList();
}

@Override
public List<ReportByExamplesFavorite> findByEverything(String providerNo, String favoriteName, String queryString) {
Query query = createQuery("ex", "ex.providerNo = ?1 AND ex.name LIKE ?2 OR ex.query LIKE ?3");
Query query = createQuery("ex", "ex.providerNo = ?1 AND ex.name = ?2 AND ex.query = ?3");
query.setParameter(1, providerNo);
query.setParameter(2, favoriteName);
query.setParameter(3, queryString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,21 +332,22 @@ public static void validateSafeSelectQuery(String sql) throws SQLException {
throw new SQLException("Potential SQL injection pattern detected");
}

if (containsSqlWord(normalized, "union")) {
String normalizedSqlSyntax = stripQuotedSqlSections(sql).toLowerCase(Locale.ROOT);
if (containsSqlWord(normalizedSqlSyntax, "union")) {
throw new SQLException("Unsafe SQL detected: UNION not permitted");
}

String[] blockedWords = {"insert", "update", "delete", "drop", "alter", "create", "truncate",
"grant", "revoke", "exec", "execute", "call", "merge", "commit", "rollback"};
for (String word : blockedWords) {
if (containsSqlWord(normalized, word)) {
if (containsSqlWord(normalizedSqlSyntax, word)) {
throw new SQLException("Unsafe SQL detected: prohibited keyword");
}
}

String[] blockedPhrases = {"into outfile", "into dumpfile", "load_file", "load data"};
for (String phrase : blockedPhrases) {
if (normalized.contains(phrase)) {
if (normalizedSqlSyntax.contains(phrase)) {
throw new SQLException("Unsafe SQL detected: prohibited keyword");
}
}
Expand Down Expand Up @@ -417,6 +418,9 @@ private boolean containsUnsafeControlToken() {
char current = sql.charAt(position);
char next = nextChar();
if (insideQuotedLiteral()) {
if (isSqlModeDependentEscape(current)) {
return true;
}
skipQuotedLiteralToken(current, next);
} else if (opensQuotedLiteral(current)) {
quote = current;
Expand All @@ -436,15 +440,17 @@ private boolean insideQuotedLiteral() {
}

private void skipQuotedLiteralToken(char current, char next) {
if (quote != '`' && current == '\\' && next != '\0') {
position++;
} else if (current == quote && next == quote) {
if (current == quote && next == quote) {
position++;
} else if (current == quote) {
quote = '\0';
}
}

private boolean isSqlModeDependentEscape(char current) {
return quote != '`' && current == '\\';
}

private static boolean opensQuotedLiteral(char current) {
return current == '\'' || current == '"' || current == '`';
}
Expand Down Expand Up @@ -615,6 +621,46 @@ private static boolean containsSqlWord(String sql, String word) {
return false;
}

/**
* Masks quoted sections while preserving input length for keyword checks.
* Backslash-containing string literals are rejected by the control-token scanner before this
* method is called because their boundaries depend on MySQL's {@code NO_BACKSLASH_ESCAPES} mode.
* This intentionally differs from the Query-by-Example validator's scanner, which must match
* JSqlParser's configured parsing. The two scanners must not be merged.
*/
private static String stripQuotedSqlSections(String sql) {
Comment thread
Ben-Heerema marked this conversation as resolved.
StringBuilder stripped = new StringBuilder(sql.length());
char quote = '\0';
int i = 0;
while (i < sql.length()) {
char current = sql.charAt(i);
char next = i + 1 < sql.length() ? sql.charAt(i + 1) : '\0';
if (quote == '\0') {
quote = sqlQuoteDelimiter(current);
stripped.append(quote == '\0' ? current : ' ');
i++;
} else if (isSqlEscapedPair(quote, current, next)) {
stripped.append(" ");
i += 2;
} else {
if (current == quote) {
quote = '\0';
}
stripped.append(' ');
i++;
}
}
return stripped.toString();
}

private static boolean isSqlEscapedPair(char quote, char current, char next) {
return current == quote && next == quote;
}

private static char sqlQuoteDelimiter(char candidate) {
Comment thread
Ben-Heerema marked this conversation as resolved.
return candidate == '\'' || candidate == '"' || candidate == '`' ? candidate : '\0';
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private static boolean startsWithSqlWord(String sql, String word) {
return sql.startsWith(word)
&& (sql.length() == word.length() || !isSqlIdentifierPart(sql.charAt(word.length())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
package io.github.carlos_emr.carlos.report.bean;

import org.owasp.encoder.Encode;
import io.github.carlos_emr.carlos.utility.MiscUtils;

public class RptByExampleQueryBean {

Expand All @@ -52,7 +51,6 @@ public RptByExampleQueryBean(int id, String query, String queryName) {
this.query = query;
this.queryName = queryName;
this.queryWithEscapeChar = Encode.forJavaScript(query);
MiscUtils.getLogger().debug("query with javascript escape char: " + queryWithEscapeChar);
}

public RptByExampleQueryBean(String providerLastName, String providerFirstName, String query, String date) {
Expand Down
Loading
Loading