-
Notifications
You must be signed in to change notification settings - Fork 842
Potential fix for code scanning alert no. 27: Query built from user-controlled sources #3930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
strehle
wants to merge
12
commits into
develop
Choose a base branch
from
Copilot-generated-fix-for-SQL-validation
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
4d2640f
Potential fix for code scanning alert no. 27: Query built from user-c…
strehle dba55c4
Potential fix for pull request finding
strehle 93467ac
Document SCIM filter SQL safety and suppress CodeQL alert #27
strehle efc3402
Apply assertSafeGeneratedSql to JdbcScimUserProvisioning#retrieveBySc…
strehle 507316a
review from copilot
strehle 4906ced
review from copilot
strehle bc48fe3
review from copilot
strehle 0ad9cf6
review from copilot
strehle 3474dbd
Merge remote-tracking branch 'origin/develop' into Copilot-generated-…
strehle 5b4382a
review from copilot
strehle 699d216
review from copilot
strehle 112326b
review from copilot
strehle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
225 changes: 225 additions & 0 deletions
225
...est/java/org/cloudfoundry/identity/uaa/resources/jdbc/AbstractQueryableSqlGuardTests.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| package org.cloudfoundry.identity.uaa.resources.jdbc; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
| import org.springframework.jdbc.core.RowMapper; | ||
| import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThatCode; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| /** | ||
| * Focused tests for {@link AbstractQueryable#assertSafeGeneratedSql(String)} — the | ||
| * defense-in-depth guard added to address CodeQL alert #27 ("Query built from | ||
| * user-controlled sources"). These tests verify that unsafe constructs are rejected | ||
| * and that typical generated SCIM filter fragments are accepted, so the guard cannot | ||
| * be silently relaxed in the future. | ||
| */ | ||
| class AbstractQueryableSqlGuardTests { | ||
|
|
||
| /** | ||
| * Tiny subclass that exposes the {@code protected static} guard for direct testing. | ||
| * No state, no behavior — just a hook to invoke the validator. | ||
| */ | ||
| private static final class GuardHarness extends AbstractQueryable<Object> { | ||
| private GuardHarness() { | ||
| super((NamedParameterJdbcTemplate) null, (JdbcPagingListFactory) null, (RowMapper<Object>) null); | ||
| } | ||
|
|
||
| static void invoke(String sqlFragment) { | ||
| assertSafeGeneratedSql(sqlFragment); | ||
| } | ||
|
|
||
| @Override | ||
| protected String getBaseSqlQuery() { | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| protected String getTableName() { | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| protected void validateOrderBy(String orderBy) { | ||
| // no-op | ||
| } | ||
| } | ||
|
|
||
| // ---------- Empty / blank fragments ---------- | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {"", " ", "\t", "\n"}) | ||
| void rejectsEmptyOrBlankFragments(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("empty SQL fragment"); | ||
| } | ||
|
|
||
| @Test | ||
| void rejectsNullFragment() { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(null)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("empty SQL fragment"); | ||
| } | ||
|
|
||
| // ---------- Statement separators / comment tokens ---------- | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| "username = :__value_0; drop table users", // statement separator | ||
| "username = :__value_0 -- and 1=1", // ANSI line comment | ||
| "username = :__value_0 # mysql line comment", // MySQL # line comment | ||
| "username = :__value_0 /* block comment */", // start of block comment | ||
| "username = :__value_0 */ trailing" // end of block comment | ||
| }) | ||
| void rejectsDisallowedTokens(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("disallowed SQL token"); | ||
| } | ||
|
|
||
| // ---------- Leading DML/DDL clauses (the keyword guard) ---------- | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| "select * from users", | ||
| "SELECT * from users", | ||
| "insert into users values (1)", | ||
| "update users set x = 1", | ||
| "delete from users", | ||
| "drop table users", | ||
| "alter table users add column x", | ||
| "create table foo(x int)", | ||
| "truncate users", | ||
| "merge into users", | ||
| "union select 1", | ||
| "with x as (select 1) select * from x", | ||
| "grant all to public", | ||
| "revoke select on users", | ||
| "exec something", | ||
| "execute something", | ||
| "call proc()", | ||
| "replace into users", | ||
| "rename table users to bar", | ||
| "comment on column foo is 'x'" | ||
| }) | ||
| void rejectsLeadingDmlDdlKeywordWithSpace(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("unexpected SQL clause"); | ||
| } | ||
|
|
||
| /** | ||
| * Bypasses raised by Copilot review: the prior implementation only blocked | ||
| * "{keyword} " (literal space). These variants must also be rejected. | ||
| */ | ||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| "SELECT\tfoo", // tab | ||
| "SELECT\nfoo", // newline | ||
| "SELECT\rfoo", // carriage return | ||
| "SELECT\ffoo", // form feed | ||
| "select(1)", // open paren, no whitespace | ||
| " select foo", // leading whitespace before keyword | ||
| "UPDATE\tusers", // mixed-case + tab | ||
| "Drop(table)" // mixed-case + paren | ||
| }) | ||
|
strehle marked this conversation as resolved.
|
||
| void rejectsKeywordFollowedByWhitespaceVariantsOrParen(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("unexpected SQL clause"); | ||
| } | ||
|
|
||
| /** | ||
| * Parenthesis-wrapped bypass raised by Copilot review: a fragment like | ||
| * "(select 1)" or "((select 1))" must be rejected — leading whitespace and | ||
| * "(" are stripped before the DML/DDL keyword check. | ||
| */ | ||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| "(select 1)", | ||
| "((select 1))", | ||
| "( select 1 )", | ||
| "(\tselect 1)", | ||
| "( ( select 1 ) )", | ||
| " (select 1) ", | ||
| "((((delete from users))))", | ||
| "(UPDATE users set x=1)", | ||
| "(DROP table users)", | ||
| "(union select 1)" | ||
| }) | ||
| void rejectsParenthesisWrappedKeyword(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("unexpected SQL clause"); | ||
| } | ||
|
|
||
| /** | ||
| * Wrapper-breakout bypass raised by Copilot review: the caller composes | ||
| * {@code "where (" + fragment + ")"}, so a fragment whose first non-whitespace | ||
| * character is {@code ")"} would close the wrapper and inject arbitrary | ||
| * boolean logic without using {@code ;}, comments, or DML/DDL keywords. | ||
| */ | ||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| ") or 1=1 or (", | ||
| ") OR 1=1 OR (", | ||
| ")", | ||
| " ) or 'a'='a' or (", | ||
| "\t) or 1=1 or (", | ||
| "\n) or 1=1 or (", | ||
| "))) or 1=1 or (((" | ||
| }) | ||
|
strehle marked this conversation as resolved.
strehle marked this conversation as resolved.
|
||
| void rejectsLeadingCloseParenWrapperBreakout(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("unexpected leading token"); | ||
| } | ||
|
|
||
| /** | ||
| * Extended wrapper-breakout bypass raised by Copilot review: a fragment can also | ||
| * close the surrounding {@code "where ("} wrapper early by starting with one or | ||
| * more {@code "("} immediately followed by {@code ")"} (e.g. {@code "()) or 1=1 or (1=1"}). | ||
| * After stripping leading whitespace and any leading {@code "("}, the next | ||
| * non-whitespace token must not be {@code ")"}. | ||
| */ | ||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| "()) or 1=1 or (1=1", | ||
| "(()) or 1=1 or ((1=1", | ||
| "((())) or 1=1", | ||
| "( )) or 1=1 or (1=1", // whitespace between ( and ) | ||
| "(\t)) or 1=1 or (1=1", // tab between ( and ) | ||
| "(\n)) or 1=1 or (1=1", // newline between ( and ) | ||
| " ()) or 1=1 or (1=1", // leading whitespace before ( | ||
| "())", // bare ()) — closes wrapper twice | ||
| // Verbatim variants from the Copilot review | ||
| "()) or 1=1 or (", | ||
| "( ) or 1=1 or (" | ||
| }) | ||
| void rejectsParenWrappedCloseParenBreakout(String fragment) { | ||
| assertThatThrownBy(() -> GuardHarness.invoke(fragment)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("unexpected leading token"); | ||
| } | ||
|
|
||
| // ---------- Typical generated SCIM filter fragments are accepted ---------- | ||
|
|
||
|
strehle marked this conversation as resolved.
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| // shapes produced by SimpleSearchQueryConverter — values are bound parameters | ||
| "username = :__value_0", | ||
| "username = :__value_0 and origin = :__value_1", | ||
| "(username = :__value_0 or origin = :__value_1) and active = :__value_2", | ||
| "lower(username) = lower(:__value_0)", | ||
| "username = :__value_0 order by created desc", | ||
| // an attribute that happens to start with a keyword-prefix (must not be rejected) | ||
| "selectable = :__value_0", | ||
| "createdby = :__value_0" | ||
| }) | ||
| void acceptsTypicalGeneratedFragments(String fragment) { | ||
| assertThatCode(() -> GuardHarness.invoke(fragment)).doesNotThrowAnyException(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.