Chapter 24 · Security, Raw SQL, Secrets, Authorization Boundaries, and Abuse Resistance

Dynamic SQL Identifiers, Sorting/Filtering APIs, Allow-Lists, and Query-Shape Abuse

Design bounded ServiceHub sort/filter/query APIs that keep dynamic behavior inside typed expressions or strict allow-lists, and treat user-controlled query shape, identifiers, wildcards, includes, and huge collections as an availability and authorization surface rather than merely an injection problem.

Advanced180–240 minutesquery-shape abuse labEF Core 10.0.11 · Microsoft.EntityFrameworkCore.Sqlite 10.0.11 · dotnet-ef 10.0.11 · .NET 10.0.11 · SDK 10.0.400Mandatory free local SQLite path · SQL Server/Azure SQL RLS and managed identity optional/provider-specificSecurity/package/platform status reviewed: August 27, 2026

Learning outcomes

01

Build dynamic sorting and filtering with reviewed expression trees or fixed query branches instead of raw identifier insertion.

02

Use strict allow-lists when SQL identifiers or fragments genuinely must be dynamic.

03

Treat user-controlled sort, filter, include, pattern, page-size, and collection cardinality as query-shape input with availability cost.

04

Bound result sets and query complexity before the database receives work.

05

Diagnose an unsafe dynamic ORDER BY API and replace it without losing functionality.

06

Test query-shape abuse with generated SQL, command counts, cancellation, timing envelopes, and authorization assertions.

1. Injection is only one way a dynamic query API can be abused

A public ServiceHub endpoint may let clients choose sort order, filters, page size, optional related data, and search patterns. Even if every value is parameterized, an attacker can still request a pathological query shape: a non-indexed sort over millions of rows, dozens of includes, a huge IN list, an unbounded page size, or a wildcard search that defeats useful indexes.

Query-shape abuse is user control over the structure or cost of a query. It can become a confidentiality problem (sorting/filtering on hidden fields), an availability problem (CPU/memory/locks/round trips), or an injection problem if the application inserts raw SQL fragments.

2. Prefer typed expression maps for sortable fields

The safest dynamic identifier is often no identifier at all. Map a small public vocabulary to compiled C# expression trees and let EF translate the selected expression.

C# · allow-listed sort vocabulary
public enum WorkOrderSort{    OpenedUtc,    Priority,    WorkOrderNumber}public static IQueryable<WorkOrder> ApplySort(    IQueryable<WorkOrder> query,    WorkOrderSort sort,    bool descending)    => (sort, descending) switch    {        (WorkOrderSort.OpenedUtc, false) => query.OrderBy(w => w.OpenedUtc),        (WorkOrderSort.OpenedUtc, true)  => query.OrderByDescending(w => w.OpenedUtc),        (WorkOrderSort.Priority, false)  => query.OrderBy(w => w.Priority),        (WorkOrderSort.Priority, true)   => query.OrderByDescending(w => w.Priority),        (WorkOrderSort.WorkOrderNumber, false) => query.OrderBy(w => w.WorkOrderNumber),        _ => query.OrderByDescending(w => w.WorkOrderNumber)    };

The client controls only an enum value. Column mapping, quoting, collation, provider syntax, and tenant filters remain EF/provider concerns. Hidden fields such as TenantId, audit metadata, or internal cost estimates simply never appear in the public sort vocabulary.

3. Build filtering from explicit operators and limits

C# · bounded filter contract
public sealed record WorkOrderQuery(    int? MinimumPriority,    DateTimeOffset? OpenedAfter,    string? Search,    WorkOrderSort Sort,    bool Descending,    int Page,    int PageSize);public static IQueryable<WorkOrder> ApplyQuery(    IQueryable<WorkOrder> query,    WorkOrderQuery request){    if (request.MinimumPriority is { } priority)        query = query.Where(w => w.Priority >= priority);    if (request.OpenedAfter is { } openedAfter)        query = query.Where(w => w.OpenedUtc >= openedAfter);    if (!string.IsNullOrWhiteSpace(request.Search))    {        var term = request.Search.Trim();        if (term.Length > 100) throw new ArgumentOutOfRangeException(nameof(request.Search));        query = query.Where(w => w.Summary.Contains(term));    }    query = ApplySort(query, request.Sort, request.Descending);    var pageSize = Math.Clamp(request.PageSize, 1, 100);    var page = Math.Max(request.Page, 1);    return query.Skip((page - 1) * pageSize).Take(pageSize);}

This is not a universal tuning prescription: the 100-character/100-row limits are example API policy for the lab. Production limits belong to capacity, product, and threat-model decisions and should be load-tested.

4. Deliberate failure: raw ORDER BY from user input

C# · WRONG: identifier/syntax injection
var sort = request.Query["sort"].ToString();var sql = $"SELECT * FROM work_orders ORDER BY {sort}";return await db.WorkOrders    .FromSqlRaw(sql)    .Take(100)    .ToListAsync(cancellationToken);

Because the sort fragment is inserted into command text, an attacker can supply SQL syntax or select a sensitive/unindexed expression. Sanitizing by deleting a semicolon is not a security model; SQL grammars offer many alternate tokens/comments/functions. The repair is a typed sort map. If raw identifiers are genuinely unavoidable, choose from a fixed dictionary of reviewed SQL fragments.

C# · raw fragment only from fixed allow-list
var allowedOrderBy = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase){    ["opened"] = "opened_utc",    ["priority"] = "priority",    ["number"] = "work_order_number"};if (!allowedOrderBy.TryGetValue(request.SortKey, out var column))    throw new ArgumentException("Unsupported sort field.", nameof(request.SortKey));var sql = $"SELECT * FROM work_orders ORDER BY "{column}" LIMIT 100";var rows = await db.WorkOrders.FromSqlRaw(sql).AsNoTracking().ToListAsync();

Here the inserted fragment can only be one of three developer-authored literals. Provider quoting differs, so prefer the typed LINQ path when portability matters.

5. Bound includes and relationship expansion

A GraphQL-like or generic “include any path” endpoint can create cartesian explosion, hidden N+1 behavior, or unauthorized data exposure. Expose reviewed shapes such as summary, withTechnician, or withRecentNotes instead of arbitrary navigation names.

User control Risk Safer contract
Arbitrary Include path cartesian explosion / hidden data enumerated response shapes + projection
PageSize=1,000,000 memory/network/database pressure server-side maximum + streaming where appropriate
Huge IN list translation/parameter/plan overhead bounded list, staging table/TVP/provider feature when justified
Leading-wildcard LIKE/regex scan/CPU cost length limits, dedicated search capability/index, timeout
Arbitrary sort/filter field data discovery / expensive plan public allow-list mapped to typed expressions

6. Cancellation and command timeout are backstops, not primary limits

Propagate request cancellation to EF and configure provider-appropriate command timeouts for bounded failure. A timeout does not make an abusive query cheap; the database may already have consumed substantial CPU, memory, I/O, or locks. Reject pathological shapes before execution and monitor timeout rate as a security/availability signal.

C# · bounded endpoint shape
public sealed record WorkOrderListItem(    int Id, string WorkOrderNumber, int Priority,    DateTimeOffset OpenedUtc, string Summary);var query = ApplyQuery(    db.WorkOrders.AsNoTracking(),    request);var rows = await query    .Select(w => new WorkOrderListItem(        w.Id, w.WorkOrderNumber, w.Priority, w.OpenedUtc, w.Summary))    .ToListAsync(httpContext.RequestAborted);

7. Observe shape, count, plan and authorization separately

ToQueryString() shows the translated SQL shape. A command interceptor can count round trips and record duration. A database plan shows whether indexes are used. Authorization tests prove the public vocabulary cannot request hidden tenant/audit fields. No one signal substitutes for the others.

Example abuse-test evidence
request: sort=priority, pageSize=100translated: ORDER BY priority ... LIMIT @pcommands: 1rows: <= 100tenant predicate: presentsensitive columns selected: noSQLite plan: reviewed for lab indexproduction provider plan: required before portability/performance claim

8. Mandatory lab: attack the query surface

  1. Expose the typed WorkOrderQuery contract and seed at least 5,000 disposable SQLite rows with skewed priorities.
  2. Verify all allowed sort values produce one bounded query and no hidden fields.
  3. Send invalid sort keys/operators, a 10,000-character search term, negative page numbers, and a very large page size; assert they are rejected or clamped by explicit policy.
  4. Run the deliberately unsafe raw ORDER BY version only against the disposable database and capture its command text.
  5. Replace it with the typed/allow-listed implementation.
  6. Use cancellation and a conservative lab timeout to test bounded failure, then inspect plan/command-count evidence.
  7. Delete the disposable database and retain only sanitized diagnostics.

9. Production judgment and bridge

A secure data API constrains both values and shape. Typed expressions, narrow DTOs, bounded cardinality, allow-listed capabilities, cancellation, rate/authorization controls, and provider plan evidence work together. Lesson 3 moves to the next security boundary: the credentials and connection configuration that give the application database authority in the first place.

Check your understanding

  1. Why is a parameterized query still vulnerable to query-shape abuse?
  2. What is preferable to inserting a dynamic column name into SQL?
  3. Is stripping semicolons sufficient sanitization for raw identifiers?
  4. Why bound page size before execution?
  5. What does a command timeout prove?
  6. Why test authorization and query plans separately?
Review the answers

1. Parameters protect values from becoming syntax, but a client can still request expensive or sensitive sorts, filters, includes, patterns, cardinalities, and result sizes if the API exposes them.

2. Map a small public vocabulary to typed LINQ expressions or fixed query branches.

3. No. SQL has many syntactic forms; use a strict allow-list of known developer-authored fragments.

4. It limits result materialization/network/database work and reduces denial-of-service risk; the exact bound must be capacity-tested.

5. Only that execution is bounded after the timeout policy; it does not make an expensive query harmless or validate its plan.

6. A query can be fast but unauthorized, or authorized but catastrophically expensive; they are different contracts.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n