2

I encountered an issue while attempting to modify my SQL query to address warnings about potential SQL injection. I aimed to parameterize the query and use raw string literals, but I received an error message instead.

It seems the error is related to the case sensitivity of my column names, and unfortunately, I can't alter the entire database to lowercase them.

I've experimented with various other approaches, but they either resulted in errors or failed to correctly retrieve the UserId.

Below is the code I tried.

  1. Original Version (SQL injection problem)
await dbContext.Database.ExecuteSqlRawAsync(
$@"
    DO $$ 
    BEGIN 
        IF (SELECT COUNT(*) FROM ""Notification"" WHERE ""UserId"" = '{userId}') > 20 THEN
            DELETE FROM ""Notification""
            WHERE ""UserId"" = '{userId}' AND ""IsReceived"" = 'TRUE' AND ""ContentId"" NOT IN (
                SELECT ""ContentId"" FROM ""Notification""
                WHERE ""UserId"" = '{userId}'
                ORDER BY ""CreatedAt"" DESC
                LIMIT 20
            );
        END IF;
    END $$"
);
  1. Raw string literals Version

(error: Npgsql.PostgresException : 42703: column "userid" does not exist)

var param = new NpgsqlParameter("@UserId", userId);
await dbContext.Database.ExecuteSqlRawAsync(
"""
    DO $$ 
    BEGIN 
        IF (SELECT COUNT(*) FROM "Notification" WHERE "UserId" = @UserId) > 20 THEN
            DELETE FROM "Notification"
            WHERE "UserId" = @UserId AND "IsReceived" = 'TRUE' AND "ContentId" NOT IN (
                SELECT "ContentId" FROM "Notification"
                WHERE "UserId" = @UserId
                ORDER BY "CreatedAt" DESC
                LIMIT 20
            );
        END IF;
    END $$
""", param);
  1. Raw string literals Version with ExecuteSqlInterpolatedAsync

(error: Npgsql.PostgresException : 42703: column "p0" does not exist)

await dbContext.Database.ExecuteSqlInterpolatedAsync(
$"""
    DO $$ 
    BEGIN 
        IF (SELECT COUNT(*) FROM "Notification" WHERE "UserId" = {userId}) > 20 THEN
            DELETE FROM "Notification"
            WHERE "UserId" = {userId} AND "IsReceived" = 'TRUE' AND "ContentId" NOT IN (
                SELECT "ContentId" FROM "Notification"
                WHERE "UserId" = {userId}
                ORDER BY "CreatedAt" DESC
                LIMIT 20
            );
        END IF;
    END $$
""");

I'd appreciate guidance on the best way to proceed with modifications.

Thank you for your assistance!

5
  • 1
    Your #3 snippet needs quotes around the {userId} parameters. Triple quoting does not alter the case of the string, so there's something else happening in your #2 snippet. Commented Dec 27, 2023 at 20:11
  • @TimRoberts "Your #3 snippet needs quotes around the" - no it does not AFAIK. It should behave the same as 2nd one (and it basically does) - EF should create parameterized query from the FormattableString accepted by ExecuteSqlInterpolatedAsync. Otherwise it would have all the same SQL injections problems as the first one and there would be no point in introduction of such method. Commented Dec 27, 2023 at 21:13
  • @GuruStron No. He's not using Postgres substitution. Notice he's not passing parameters. He's using C# $ string substitution, so it will still have injection problems. Commented Dec 27, 2023 at 21:18
  • @TimRoberts yes there is substitution, no there is no injection problems if method is correctly used. That is the goal of RelationalDatabaseFacadeExtensions.ExecuteSqlInterpolatedAsync - "Any parameter values you supply will automatically be converted to a DbParameter." Commented Dec 27, 2023 at 21:20
  • @TimRoberts see that it accepts FormattableString, not just string. Compiler will not build the string and this allows EF to see all the {} placeholders and their values and turn them into query parameters. Commented Dec 27, 2023 at 21:23

2 Answers 2

2

The IF isn't actually necessary, so you could just use a normal SqlInterpolated block (ie fully parameterized).

await dbContext.Database.ExecuteSqlInterpolatedAsync(
$"""
    DELETE FROM "Notification"
    WHERE "UserId" = {userId}
      AND "IsReceived" = TRUE
      AND "ContentId" NOT IN (
          SELECT n2."ContentId"
          FROM "Notification" n2
          WHERE n2."UserId" = {userId}
          ORDER BY n2."CreatedAt" DESC
          LIMIT 20
      );
""");
Sign up to request clarification or add additional context in comments.

Comments

2

AFAIK you can't use parameters with DO. From the docs:

DO executes an anonymous code block, or in other words a transient anonymous function in a procedural language.
The code block is treated as though it were the body of a function with no parameters, returning void. It is parsed and executed a single time.

So it treats the parameter names as column names (and obviously fails).

So just rewrite the query so no DO is needed or create a database function and use EF to invoke it with parameters.

Also see this answer.

1 Comment

Thank you for your assistance. It turns out I misunderstood the functionality of the DO statement.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.