|
| 1 | +-- SPDX-License-Identifier: MPL-2.0 |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +||| Semantic proofs for TypedQLiser's type-safety levels. |
| 5 | +||| |
| 6 | +||| The ten levels in `Types.idr` are, on their own, abstract labels carrying a |
| 7 | +||| per-level `ProofStatus`. This module makes one of them — `InjectionFree` |
| 8 | +||| (level 5: "no string interpolation in structure", i.e. the no-SQL-injection |
| 9 | +||| guarantee) — a *real*, machine-checked property over an actual query model: |
| 10 | +||| |
| 11 | +||| 1. a minimal but faithful query AST whose value leaves are either bound |
| 12 | +||| parameters / literals (safe) or raw spliced strings (an injection vector); |
| 13 | +||| 2. `QueryInjectionFree`, the proposition that no raw splice occurs anywhere; |
| 14 | +||| 3. `decInjectionFree`, a sound + complete decision procedure returning a |
| 15 | +||| genuine proof (`Dec`), so a "Proven" InjectionFree certificate is backed |
| 16 | +||| by a constructive witness and a raw splice can never be certified safe; |
| 17 | +||| 4. a certifier whose `Proven` verdict is *proven* to entail the property |
| 18 | +||| (`certifyProvenSound`), plus positive and negative controls. |
| 19 | + |
| 20 | +module Typedqliser.ABI.Semantics |
| 21 | + |
| 22 | +import Typedqliser.ABI.Types |
| 23 | +import Data.So |
| 24 | +import Data.Vect |
| 25 | +import Decidable.Equality |
| 26 | + |
| 27 | +%default total |
| 28 | + |
| 29 | +-------------------------------------------------------------------------------- |
| 30 | +-- A minimal but faithful query model |
| 31 | +-------------------------------------------------------------------------------- |
| 32 | + |
| 33 | +||| A value appearing in a query. Only `RawSplice` — an interpolated string |
| 34 | +||| spliced into the query structure — is an injection vector; bound parameters |
| 35 | +||| and literals are safe. |
| 36 | +public export |
| 37 | +data Value : Type where |
| 38 | + Param : (idx : Nat) -> Value -- bound parameter placeholder ($1, $2, …) |
| 39 | + Lit : (n : Integer) -> Value -- literal constant |
| 40 | + RawSplice : (s : String) -> Value -- interpolated string fragment (UNSAFE) |
| 41 | + |
| 42 | +||| A WHERE predicate over columns and values. |
| 43 | +public export |
| 44 | +data Pred : Type where |
| 45 | + Cmp : (col : String) -> (v : Value) -> Pred |
| 46 | + And : Pred -> Pred -> Pred |
| 47 | + Or : Pred -> Pred -> Pred |
| 48 | + |
| 49 | +||| A SELECT query. |
| 50 | +public export |
| 51 | +data Query : Type where |
| 52 | + Select : (table : String) -> (cols : List String) -> (where_ : Pred) -> Query |
| 53 | + |
| 54 | +-------------------------------------------------------------------------------- |
| 55 | +-- InjectionFree as a genuine proposition (no RawSplice node anywhere) |
| 56 | +-------------------------------------------------------------------------------- |
| 57 | + |
| 58 | +||| A value is injection-safe unless it is a raw splice. There is deliberately |
| 59 | +||| no constructor for `RawSplice`, so `ValueOk (RawSplice s)` is uninhabited. |
| 60 | +public export |
| 61 | +data ValueOk : Value -> Type where |
| 62 | + ParamOk : ValueOk (Param i) |
| 63 | + LitOk : ValueOk (Lit n) |
| 64 | + |
| 65 | +||| No `ValueOk` witnesses a raw splice — the heart of the guarantee. |
| 66 | +public export |
| 67 | +Uninhabited (ValueOk (RawSplice s)) where |
| 68 | + uninhabited ParamOk impossible |
| 69 | + uninhabited LitOk impossible |
| 70 | + |
| 71 | +||| A predicate is injection-free when every comparison value is safe. |
| 72 | +public export |
| 73 | +data PredInjectionFree : Pred -> Type where |
| 74 | + CmpFree : ValueOk v -> PredInjectionFree (Cmp col v) |
| 75 | + AndFree : PredInjectionFree p -> PredInjectionFree q -> PredInjectionFree (And p q) |
| 76 | + OrFree : PredInjectionFree p -> PredInjectionFree q -> PredInjectionFree (Or p q) |
| 77 | + |
| 78 | +||| A query is injection-free when its WHERE predicate is. |
| 79 | +public export |
| 80 | +data QueryInjectionFree : Query -> Type where |
| 81 | + SelectFree : PredInjectionFree w -> QueryInjectionFree (Select t c w) |
| 82 | + |
| 83 | +-------------------------------------------------------------------------------- |
| 84 | +-- Sound + complete decision procedure (returns a real proof) |
| 85 | +-------------------------------------------------------------------------------- |
| 86 | + |
| 87 | +public export |
| 88 | +decValueOk : (v : Value) -> Dec (ValueOk v) |
| 89 | +decValueOk (Param i) = Yes ParamOk |
| 90 | +decValueOk (Lit n) = Yes LitOk |
| 91 | +decValueOk (RawSplice s) = No absurd |
| 92 | + |
| 93 | +public export |
| 94 | +decPredFree : (p : Pred) -> Dec (PredInjectionFree p) |
| 95 | +decPredFree (Cmp col v) = case decValueOk v of |
| 96 | + Yes ok => Yes (CmpFree ok) |
| 97 | + No no => No (\(CmpFree ok) => no ok) |
| 98 | +decPredFree (And p q) = case decPredFree p of |
| 99 | + No np => No (\(AndFree pp _) => np pp) |
| 100 | + Yes pp => case decPredFree q of |
| 101 | + Yes qq => Yes (AndFree pp qq) |
| 102 | + No nq => No (\(AndFree _ qq) => nq qq) |
| 103 | +decPredFree (Or p q) = case decPredFree p of |
| 104 | + No np => No (\(OrFree pp _) => np pp) |
| 105 | + Yes pp => case decPredFree q of |
| 106 | + Yes qq => Yes (OrFree pp qq) |
| 107 | + No nq => No (\(OrFree _ qq) => nq qq) |
| 108 | + |
| 109 | +||| The headline decision procedure: decide injection-freedom, returning a |
| 110 | +||| genuine `QueryInjectionFree` witness when it holds. |
| 111 | +public export |
| 112 | +decInjectionFree : (q : Query) -> Dec (QueryInjectionFree q) |
| 113 | +decInjectionFree (Select t c w) = case decPredFree w of |
| 114 | + Yes ok => Yes (SelectFree ok) |
| 115 | + No no => No (\(SelectFree ok) => no ok) |
| 116 | + |
| 117 | +-------------------------------------------------------------------------------- |
| 118 | +-- Certifier soundness: a `Proven` InjectionFree status is never a lie |
| 119 | +-------------------------------------------------------------------------------- |
| 120 | + |
| 121 | +||| Certify the InjectionFree (level 5) obligation for a query. `Proven` is |
| 122 | +||| emitted only when the decision procedure produced a real proof. |
| 123 | +public export |
| 124 | +certifyInjectionFree : (q : Query) -> ProofStatus |
| 125 | +certifyInjectionFree q = case decInjectionFree q of |
| 126 | + Yes _ => Proven |
| 127 | + No _ => Refuted |
| 128 | + |
| 129 | +||| Soundness: if the certifier reports `Proven`, the query really is |
| 130 | +||| injection-free. (Together with `decInjectionFree` being a `Dec`, this also |
| 131 | +||| means a query that is *not* injection-free can never be reported `Proven`.) |
| 132 | +export |
| 133 | +certifyProvenSound : (q : Query) -> certifyInjectionFree q = Proven -> QueryInjectionFree q |
| 134 | +certifyProvenSound q prf with (decInjectionFree q) |
| 135 | + certifyProvenSound q prf | Yes ok = ok |
| 136 | + certifyProvenSound q Refl | No _ impossible |
| 137 | + |
| 138 | +||| `InjectionFree` is level 5 — the certified obligation is the fifth of ten. |
| 139 | +export |
| 140 | +injectionFreeIsLevelFive : levelNat InjectionFree = 5 |
| 141 | +injectionFreeIsLevelFive = Refl |
| 142 | + |
| 143 | +-------------------------------------------------------------------------------- |
| 144 | +-- Positive control: a parameterized query is provably injection-free |
| 145 | +-------------------------------------------------------------------------------- |
| 146 | + |
| 147 | +||| `SELECT id, name FROM users WHERE id = $1` — fully parameterized. |
| 148 | +public export |
| 149 | +safeQuery : Query |
| 150 | +safeQuery = Select "users" ["id", "name"] (Cmp "id" (Param 1)) |
| 151 | + |
| 152 | +||| Machine-checked: the parameterized query is injection-free. |
| 153 | +export |
| 154 | +safeQueryInjectionFree : QueryInjectionFree Semantics.safeQuery |
| 155 | +safeQueryInjectionFree = SelectFree (CmpFree ParamOk) |
| 156 | + |
| 157 | +||| …and the certifier reports `Proven` for it (computes to `Proven`). |
| 158 | +export |
| 159 | +safeQueryCertifiesProven : certifyInjectionFree Semantics.safeQuery = Proven |
| 160 | +safeQueryCertifiesProven = Refl |
| 161 | + |
| 162 | +-------------------------------------------------------------------------------- |
| 163 | +-- Negative control: an interpolated query CANNOT be certified injection-free |
| 164 | +-------------------------------------------------------------------------------- |
| 165 | + |
| 166 | +||| `SELECT id FROM users WHERE name = '<spliced user input>'` — a classic |
| 167 | +||| injection vector built by string interpolation. |
| 168 | +public export |
| 169 | +unsafeQuery : Query |
| 170 | +unsafeQuery = |
| 171 | + Select "users" ["id"] (Cmp "name" (RawSplice "'; DROP TABLE users;--")) |
| 172 | + |
| 173 | +||| Machine-checked: there is **no** proof that the interpolated query is |
| 174 | +||| injection-free. This is what makes the guarantee non-vacuous — `ValueOk` |
| 175 | +||| has no constructor for a raw splice, so the property is genuinely refuted. |
| 176 | +export |
| 177 | +unsafeQueryNotInjectionFree : Not (QueryInjectionFree Semantics.unsafeQuery) |
| 178 | +unsafeQueryNotInjectionFree (SelectFree (CmpFree ok)) = absurd ok |
0 commit comments