Skip to content

Latest commit

 

History

History
731 lines (614 loc) · 54.2 KB

AUDIT.md

File metadata and controls

731 lines (614 loc) · 54.2 KB

AUDIT — Inventaire complet du pack php/

But : fork propre, fonctionnel, mergeable un jour dans github/codeql. Zéro bricolage, zéro cas-par-cas : la sémantique PHP est modélisée au plus bas niveau (extractor/AST/CFG/SSA/dataflow), la couverture (noms de fonctions dangereuses) est de la DATA (MAD).

Reprise après crash : ce fichier est le journal d'audit + le plan de remédiation. Chaque item du plan (§P) a un ID, un statut (☐ TODO / ◐ WIP / ☑ DONE) et se commit séparément. Historique : git log --oneline -- AUDIT.md. État du code : git log --oneline -5.

Audit réalisé le 2026-07-02 par 6 agents parallèles (read-only) comparant chaque couche aux conventions upstream de ruby/. CLI utilisé : CodeQL 2.25.6 (dans .tooling/, git-ignoré).


VERDICT GLOBAL : non-mergeable en l'état — socle sain, finition « recherche »

Le squelette (réutilisation de shared/ : tree-sitter-extractor, controlflow, ssa, dataflow) est idiomatique et propre. TypeInference.qll est réellement principiel. Mais 3 familles de problèmes bloquent le merge et violent la règle « pas de cas-par-cas » :

  1. Bricolage de soundness dans CFG/SSA — un hack uncertain-writes qui casse les strong-updates (→ FP), des mises à jour d'élément qui tuent la variable entière (→ FN), et ~8 constructions de contrôle linéarisées (for/foreach/switch/match/court-circuit/try-catch/complétions anormales).
  2. Cas-par-cas hardcodé en QL — sources/sinks/sanitizers/steps des builtins PHP écrits en listes de chaînes dans .qll au lieu d'être de la DATA (MAD). Trois pièces moteur (PostUpdateNode, lambda*, SanitizerGuard) stubbées à none() et rustinées par des taint-steps par-pattern.
  3. Hygiène de packaging/merge — binaire extractor committé, php/ absent du workspace Cargo & Bazel, aucun .qhelp, suites non-standard, MAD sans colonne provenance, 5 requêtes important des modules internal, docs-diary + README écrasé à la racine.

Aucun problème n'est dans la plomberie des libs partagées — tout est dans les choix de modélisation et l'emballage. C'est réparable proprement, mais c'est un chantier structurel, pas du polish.

Tableau de gravité (compte des findings)

Axe BLOQUANT-MERGE HACK INCOMPLET DOC-PÉRIMÉE
1. Extractor + packaging 4 3 2 3
2. AST 3 3 3 4
3. CFG + SSA 3 2 4 3
4. DataFlow / Taint / TypeInf 4 4 4 2
5. Requêtes + tests + MAD 4 — 3 2
6. Docs / hygiène 5 — 2 6

§1 — Extractor Rust + packaging (verdict : Rust propre, packaging non-mergeable)

Positifs : main/extractor/generator/autobuilder.rs calquent ruby et réutilisent shared/tree-sitter-extractor sans réimplémenter. Aucun chemin machine hardcodé dans php/. dbscheme réellement généré (generator.rs, en-tête « do not edit ») → pas de risque de maintien manuel. php.dbscheme.stats réel (598 Ko). qlpacks auto-découverts par codeql-workspace.yml.

BLOQUANT-MERGE

  • php/tools/linux64/extractor — binaire ELF 6.5 Mo committé (git ls-files le suit). Upstream ne commit jamais d'extracteur buildé. → supprimer du git, .gitignore, produire par le build.
  • Cargo.toml:5-16 — php/extractor absent des members du workspace ; Cargo.lock n'a aucune entrée codeql-extractor-php/tree-sitter-php. php/build.sh:11 (cargo build -p …) ne résout pas depuis la racine. → ajouter "php/extractor" aux members, régénérer Cargo.lock.
  • Aucun câblage Bazel : pas de php/BUILD.bazel ni php/extractor/BUILD.bazel ; tree-sitter-php non enregistré dans le vendoring cargo Bazel. Tous les langages upstream buildent via Bazel. → ajouter les cibles + le crate.
  • php/codeql-extractor.yml:37 — TRAP compression ^(none|gzip|brotli)$ alors que le shared ne supporte que none|gzip|zstd. brotli est inparsable, zstd inatteignable. → aligner sur l'enum.

HACK / INCOMPLET

  • php/build.sh + absence de php/scripts/create-extractor-pack.sh — script de build maison au lieu du flux standard create-extractor-pack.sh + Bazel. → remplacer.
  • php/php.dbscheme (racine) duplique php/ql/lib/php.dbscheme (ruby ne commit que celui du pack). → cesser de committer la copie racine, la générer au build.
  • Overlay à moitié câblé : extractor.rs:75,135,163 lisent les env _OVERLAY_* mais codeql-extractor.yml n'a pas overlay_support_version ni qlpack.yml: compileForOverlayEval → code mort. → câbler ou retirer.
  • php/analyze.sh — wrapper hors-pack sans analogue ruby. → sortir du pack.

STYLE : Cargo.toml authors=["PHPcodeQl"] (ruby ["GitHub"]) ; [[bin]] explicite superflu ; tools/index-files.sh/autobuild.sh sans shebang ni set -eu.


§2 — Couche AST (verdict : non-mergeable — API fuit les types générés, couverture partielle)

Positifs : Locations.qll/FileSystem.qll sont byte-identiques à ruby (génériques propres, pas hackés). AstNode fournit un getParent()/getAChild() normalisé (les enfants tree-sitter bruts ne fuitent pas à la base). Aucun nom de classe/fonction hardcodé dans la couche AST (correct).

BLOQUANT-MERGE

  • Class.qll:132 resolveClassReference(Php::AstNode) — prédicat public prenant un type généré Php::AstNode en signature → fuite du type interne dans l'API publiée. Idem getAUsedTrait (Class.qll:88). → n'exposer que des wrappers publics ; Php::* strictement privé.
  • Pas de hiérarchie publique synthétisée — chaque wrapper est extends AstNode instanceof Php::X ; tout cast consommateur tombe sur Php::*. Ruby n'expose jamais les types générés. → adopter le pattern ruby (Generated::* interne + classes publiques synthétisées).
  • Couverture incomplète (nœuds sécurité-critiques manquants) : Include/Require(_Once) (LFI/RFI), ShellCommandExpression (backtick RCE), Eval/Exit, MemberAccess/ScopedPropertyAccess/ ClassConstantAccess, Property/Const/EnumCase declarations, attributs PHP8, ArrayCreation/Pair/ ListLiteral, For/Do/Switch/Case/Try/Catch/Finally/Throw/Break/Continue/Match, ternaire, unaires/update/augmented, Clone/Yield/Print, Heredoc/Nowdoc, use-clause de closure, use top-level. → wrapper systématique table-driven contre TreeSitter.qll.

HACK

  • Naming.qll:12 simpleNameOf — résolution par dernier segment de nom (string-match) ; confond deux classes de même nom court. → nom pleinement qualifié calculé une fois.
  • Class.qll:136-142 — use-alias : ne gère que les alias (use X as Y), via toString() d'un nœud, et même fichier seulement ; les imports non-aliasés (use App\Foo; puis Foo), groupes, relatifs, \-global non gérés. → table d'imports par fichier, résolution uniforme.
  • Callable.qll:57 getDeclaringType() retourne AstNode via getParent().getParent() positionnel (devrait être ClassLike) ; idem accès positionnels Expr.qll:81-84, Call.qll:26.

INCOMPLET : closures sans classes exposant les captures ; Method sans visibilité/final/type de retour ; Class sans getConstant/getProperty/getEnumCase ; Namespace.qll:67 sans relatifs/\.

DOC/STYLE : php.qll:6 en-tête « Phase 1 raw AST » périmé (CFG/dataflow importés) ; toString() overrides redondants (Expr.qll:8, Stmt.qll:9).


§3 — CFG + SSA (verdict : NON-mergeable — vrais bugs de soundness, pas juste incomplétude)

Positifs : usage idiomatique du shared codeql.controlflow.Cfg (Completion/CfgScope/Make<>, PostOrder/StandardPreOrder/Leaf trees) et du shared codeql.ssa.Ssa via InputSig. La plomberie est propre — l'unsoundness est entièrement dans les choix de modélisation.

BLOQUANT-MERGE

  • SsaImpl.qll:155-177 — hack uncertain-writes détruit les strong-updates. inConditionalBranch ⇒ certain=false marque tout write en branche comme may-write : il ne tue jamais un def antérieur. Dans une même branche if($c){ $y=$_GET['x']; $y="safe"; sink($y); }, la 2e écriture (sanitisante) est uncertain → le taint survit → FP, indépendant de tout join. Ruby n'utilise certain=false que pour de vrais may-writes (captures). → tous les writes syntaxiques certain=true ; s'appuyer sur les vrais φ.
  • ControlFlowGraphImpl.qll:230-243 (StructuralTree) — branchement massivement manquant : for, foreach, switch/case/default, match, court-circuit && || ??, break/continue/return/ throw, try/catch/finally — tous présents dans l'AST mais linéarisés. Conséquences : pas de back-edge for/foreach (taint de boucle uniquement via le hack) ; pas d'isolation de case (write case-N → read case-M = FP) ; pas d'arête throw→catch (param de catch inatteignable = FN) ; code après return/throw traité comme vivant (FP code-mort). → Trees branchants par construction, calqués ruby.
  • SsaImpl.qll:70-91,116-118 — $a[k]=v / $o->p=v = strong-update tuant la racine. updateBaseVariable fait un write certain de tout $a, tuant son def antérieur. Donc $a=$_GET; $a['x']='safe'; sink($a['y']); perd le taint → FN/unsound. Une mutation partielle doit être un weak-write (uncertain) ou read+partial-def, jamais un kill.

HACK

  • SsaImpl.qll:168-177 inConditionalBranch — special-casing par ancêtre AST (node-type case-by-case), découplé de la structure CFG réelle. Redondant pour if/while/do (φ via definitionReachingValue) → double-modélisation, provenance ambiguë, chemins dupliqués. Retrait : supprimer le prédicat, tous les writes certain=true. Risque de régression : switch/match/for/foreach reposent uniquement sur ce hack aujourd'hui → le retrait doit être gated sur l'atterrissage de leurs Trees branchants.
  • DataFlowPrivate.qll:309-315 definitionReachingValue — marche φ-input récursive ; correcte en soi, mais empilée sur le hack = 2e moitié du double-modèle.

INCOMPLET

  • ControlFlowGraphImpl.qll:23,78-82 — ReturnCompletion/TReturnCompletion jamais produits (code mort) ; return non modélisé comme complétion anormale.
  • CfgConsistency.ql n'assure que « chaque Stmt/Expr a un CfgNode » → passe malgré l'absence d'arêtes de branche : fausse assurance, ne détecte aucun des trous ci-dessus.
  • SsaImpl.qll:179-186 — LHS d'un augmented-assign ($x .= …) écrit mais pas lu → taint de l'ancienne valeur perdu = FN.
  • Tests CFG minces : cfg/flow.php = une fonction linéaire ; aucun test négatif pour le FP strong-update du hack, aucun pour switch/match/for/foreach/try.

DOC-PÉRIMÉE : ControlFlowGraphImpl.qll:244-252 — NOTE mensongère (« if branching disproven / linéarisé ») contredite 30 lignes plus bas par IfTree/IfElseTree. En-têtes :9-10 / SsaImpl:8-9 listent params/foreach/global/static comme « not yet modelled » alors qu'ils sont implémentés.


§4 — DataFlow / TaintTracking / TypeInference (verdict : non-mergeable — cas-par-cas hardcodé)

Positifs (à garder) : TypeInference.qll principiel (new/$this/params typés/SSA/retours typés/fluent return $this/props typées/clone/dynamique new $c), récursif avec fallback par nom. simpleLocalFlowStep via SSA + fix φ (:309-315) correct et général. structuralPropagator (TaintTrackingPrivate:462-481) = le bon pattern « énumérer une fois, composer récursivement » — le modèle de ce que les listes hardcodées devraient devenir.

BLOQUANT-MERGE (le cœur du « pas de cas-par-cas »)

  • security/FlowSources.qll:11-221 — tous les builtins cœur hardcodés en QL, pas en MAD : isRemoteSource (superglobales + getenv/filter_input + liste de noms de méthodes), isSanitizerFunction:54-73 (~35 noms), sinkFunctionKind:86-113 (chaque builtin dangereux→kind), listes call_user_func/array_map. Le MAD existe déjà (ModelExtensions.qll + ext/*.yml ne portent que le framework). → livrer ext/php-builtins.model.yml, supprimer les listes QL ; le moteur ne garde que le glue générique. Plus gros blocker.
  • TaintTrackingPrivate.qll:107-117 propagatingBuiltin + :409 (HO-callbacks) + :425 (parse_str) + :399 (accesseurs d'exception) — couverture taint cuite dans le .qll. strtoupper/trim/substr… = stepModel ; array_map/usort/call_user_func, parse_str = summaries. → tous les noms en .model.yml.
  • Concepts.qll:54 SanitizerGuard = code mort (référencé nulle part ; les requêtes n'utilisent que isBarrier = n instanceof Sanitizer). Sanitisation par branche (if(ctype_alnum($x)), in_array, ===) non modélisée. → câbler guard→barrier (over-approx OK) ou retirer la classe + sa doc.

HACK (patches par-pattern tenant lieu de pièces moteur → FN silencieux)

  • DataFlowPrivate.qll:220-224 PostUpdateNode = none() — mutation-après-appel non modélisée. Papier par des taint-steps étroits : setter (:335-356), by-ref &$r (:236-254, fonctions seulement, pas méthodes), parse_str (:425). Tout pattern de mutation non énuméré = FN. → implémenter PostUpdateNode/getPreUpdateNode sur receveurs + &-args, laisser le moteur porter le flux inverse.
  • DataFlowPrivate.qll:410-418 lambda* = none() — flux d'ordre supérieur via le moteur absent ; seules les closures inline passées à une liste de builtins hardcodée marchent. Callback stocké en var / passé à une fonction user qui l'appelle = FN. → lambdaCreation/lambdaCall généraux.
  • Named-args = taint-only + fonctions seulement. getArgumentCfgNode:79-83 retire correctement les named args du mapping positionnel, mais le step de reconnexion (:437-450) ne matche que FunctionCall→FunctionDefinition. Named args vers méthodes/statique + tout le flux pure-DataFlow = FN. → mapper named→positional dans la couche arg/parameterMatch pour tous les types d'appel.
  • viableCallable:170-173 — coupe le fallback par nom dès qu'un type de receveur est inféré, même si la classe n'a pas la méthode (__call, sous-classe sous-approximée). → getTypedCallee vide et fallback bloqué = arête perdue = FN. Sur-précision violant recall-first. → gater le fallback sur « un callee typé a réellement été trouvé », pas sur « le type existe ».

INCOMPLET / précision

  • TFieldContent(name) clé par nom global (:240-242,355-379) → confond $a->id et $b->id en interprocédural. → approx par (classe, champ) quand le type est inféré.
  • $GLOBALS['k'] et alias =& restreints même fichier (:358-385) → cross-file = FN. → jump-steps par nom/clé.
  • CastNode = none() ; flux ctor-arg → propriété promue absent (new C($t) puis $o->f) = FN.

DOC-PÉRIMÉE : Concepts.qll:50-57 documente SanitizerGuard comme point d'extension vivant (inerte) ; FlowSources.qll:223,231,240 prétend « extensible via data » alors que le set autoritatif est le QL hardcodé.


§5 — Requêtes + tests + MAD (verdict : non-mergeable — qhelp/suites/MAD)

Positifs : les 10 requêtes taint cœur (Command/Sql/Code injection, XSS, Path, OpenRedirect, SSRF, LDAP, FileInclusion, UnsafeDeser) sont uniformes, métadonnées correctes (@id php/…, @kind path-problem, @security-severity, tags CWE), et sourcent les sinks depuis Sink.getKind() + MAD, pas de hardcode dans le .ql. Tests = vraies données path-graph (edges/nodes/#select), cas safe présents et correctement exclus. Prédicats extensibles ext/*.yml ↔ ModelExtensions.qll cohérents, pas de ligne malformée.

BLOQUANT-MERGE

  • Les 15 requêtes sans .qhelp (find → aucun). Upstream rejette toute requête sécurité sans qhelp. → un <Query>.qhelp par requête (overview/recommendation/example/references).
  • Suites : seul php-security.qls, format non-standard (hand-code include: kind/tags, queries: .). Manque php-code-scanning.qls, php-security-extended.qls, php-security-and-quality.qls. Le code-scanning ne les trouvera pas. → suites standard via suite-helpers.
  • MAD = réimplémentation privée, pas le shared MaD. ModelExtensions.qll:19-36 déclare des sourceModel(3col)/sinkModel(4col)/… maison sans colonne provenance, sans neutralModel/typeModel, sans ModelValidation → n'interopère pas avec l'outillage MaD upstream. → migrer vers les extensibles codeql/dataflow standard (colonnes type/path/kind/provenance) + ModelValidation.
  • 5 requêtes src importent internal : SemgrepAudit, LaravelRoutes, TypeJuggling, HardcodedCryptoKey, CallResolutionCoverage importent …ast.internal.TreeSitter / …internal.TypeInference. Violation de couche interdite upstream. → exposer les classes nécessaires en API publique.

INCOMPLET

  • ReflectedXss rate print() : ReflectedXss/test.php:3 annoté // BUG (print($_POST['x'])) mais absent du .expected (seul le echo ligne 2 y est). Trou de couverture masqué par un test vert. → ajouter print comme sink XSS, MAJ expected.
  • SemgrepAudit.ql émet des lignes dupliquées (un nom → plusieurs ruleIds) ; @precision low, sans @security-severity, très bruyant. → dédupliquer.
  • TypeJuggling/SemgrepAudit/LaravelRoutes hardcodent des noms en QL (listes hash/banned/verbes). → MAD.

STYLE : frameworks.model.yml:1-13 en-tête parle d'un summaryModel inexistant (le vrai est stepModel) ; whereRaw dupliqué (frameworks:41, laravel:36) ; source HardcodedCryptoKey en string-shape TreeSitter inline plutôt qu'un Concept réutilisable.


§6 — Docs / hygiène de merge (verdict : docs périmées & contradictoires, à relocaliser)

BLOQUANT-MERGE

  • DEV.md:32,40,45,46 — tous les chemins pointent vers /home/vozec/Desktop/r&d/PHPcodeQl/… (n'existe plus). Un agent frais échoue ligne 1. → réécrire avec la vraie racine + note « obtenir le CLI séparément ».
  • 7 .md diary à la racine (PROJECT_STATUS, STRUCTURAL_ROADMAP, IMPROVEMENTS, THREAT_MODEL, PLAN, AUDIT, DEV) — racine upstream n'a rien de tel. → déplacer sous php/docs/ ; exclure AUDIT.md du merge.
  • README.md racine écrasé par un README de pack PHP → clobbe le README du monorepo. → php/README.md, restaurer le README racine dans la branche de merge.
  • bench/ (perf/scoring semgrep) = scratch recherche. → sous php/ ou hors branche de merge.
  • Aucune change-note php — les autres packs livrent <lang>/ql/{lib,src}/change-notes/. → ajouter.

DOC-PÉRIMÉE (claims faux vs code)

  • Compte de tests : docs disent « 43 (30+15) » ; réel = 45 (30 query + 15 library ; 30+15≠43). IMPROVEMENTS dit 27 puis 44. 4 chiffres différents ; vérité = 45.
  • while/do « à finir » (PROJECT_STATUS:92) alors qu'implémentés + LoopTaint.expected vert.
  • NOTE ControlFlowGraphImpl.qll:244-252 (déjà en §3).
  • DVWA incohérent : 50 / 44 / 17 selon les docs ; FP 6 / 3 / 4. Invérifiable ici, docs se contredisent.
  • DEV.md = phase-log figé en Phase 5 (« dataflow pas encore câblé », « frameworks futurs ») alors que tout est présenté comme livré ailleurs.
  • « 13 requêtes sécurité » : 13 .ql dans Security/ mais un est SemgrepAudit (helper), la liste en nomme 12. Réel : 12 sécurité + 3 utilitaires.

Vérifiés corrects : Rust 251 LOC ✓, 7 MAD ✓.


§P — PLAN DE REMÉDIATION (exécution item par item, un commit chacun)

Ordre = dépendances techniques d'abord (CFG avant retrait du hack), puis dette DATA, puis hygiène. Règle : chaque item = test qui échoue (rouge) → fix général → suite verte → commit. Jamais de patch cas-par-cas. Statuts : ☐ TODO · ◐ WIP · ☑ DONE.

Journal de progression (handoff) — MAJ 2026-07-03

Fait & poussé :

  • P0 ✓ ; Phase A COMPLÈTE (A.1–A.7) ✓ — tout le flux de contrôle branche, hack retiré.
  • Phase B ✓ (large) : B.1 by-ref méthodes ; B.2 lambdas + string-callables + array-HO ; B.3 named-args méthodes ; B.4 dispatch fallback + dispatch virtuel interface/abstrait ; B.5 barrier guards ; B.6 $GLOBALS cross-file. + fix ordre d'assignation $a=f($a), spread f(...$args), récursivité vérifiée (DeepChain, 100 hops).
  • Phase C ✓ : builtins (sources/sinks/sanitizers/steps) + méthodes framework + array-HO migrés en MAD (ext/php-builtins.model.yml, ext/modern-frameworks.model.yml) ; SSTI + moderne ajoutés en DATA (ext/templating.model.yml, TemplateInjection.ql, Guzzle SSRF). Moteur = logique seule.
  • Phase E ✓ : qhelp pour les 13 requêtes, suites standard (code-scanning/security-extended/ security-and-quality), imports internal retirés des requêtes sécurité, fix print XSS.
  • Phase F partiel : TRAP pattern (zstd), binaire décommité, change-notes, php/extractor dans le workspace Cargo (build vérifié).
  • Suite : 71 tests verts (départ ~45).

Reste (gros items infra, à faire en effort dédié) :

  • Phase D — hiérarchie AST publique : synthétiser des wrappers publics et rendre Php::* privé (fuite resolveClassReference(Php::AstNode) Class.qll:132 ; wrappers de nœuds manquants). Refactor large et transverse — à faire prudemment (ne pas casser le pack). Résolution de noms principielle (FQN).
  • Phase F — Bazel : php/BUILD.bazel + php/extractor/BUILD.bazel calqués sur ruby, ET enregistrer tree-sitter-php dans le vendoring cargo Bazel (misc/bazel/3rdparty/… via update_cargo_deps.sh) — nécessite la toolchain Bazel. Le build Cargo marche déjà (dev) ; Bazel = build CI/release.
  • Phase F — docs : relocaliser les .md diary sous php/docs/, restaurer README racine, sortir bench//AUDIT.md — step de branche-merge (les docs servent le handoff actuel, à ne pas faire tant qu'on travaille dessus).
  • B.1 fin : PostUpdateNode général (notes ci-dessous) ; B.6 fin : content par (classe,champ). Baseline & commandes : DEV.md + §7. CLI local : .tooling/codeql/codeql (2.25.6, git-ignoré).

Phase 0 — Vérité de base & filet (rapide, non-risqué) — ☑ FAIT (45/45 verts, sans warning)

  • P0.1 ☑ DEV.md réparé (chemins réels + note CLI + commande de test) ; compteurs corrigés (tests=45) dans PROJECT_STATUS/README/IMPROVEMENTS ; while/do marqués faits ; piège r&d-quoting retiré.
  • P0.2 ☑ NOTE mensongère ControlFlowGraphImpl.qll:244-252 supprimée ; en-têtes CFG & SsaImpl réécrits pour refléter le réel.
  • P0.3 ☑ Baseline consigné (§7) : All 45 tests passed (CLI 2.25.6).
  • P0.4 ☑ CfgConsistency.ql durci : nouvel invariant linearisedBranch (toute construction qui doit brancher mais dont aucun nœud n'a ≥2 successeurs est signalée). Compile ✓ ; suite 45/45 (non-régressif).
  • P0.5 ☑ Warning cast redondant TaintTrackingPrivate.qll:349 supprimé.

Phase A — CFG complet & correct (débloque le retrait du hack ; cœur soundness)

Méthode test-first, un Tree branchant par construction, calqué sur ruby.

  • A.1 ☑ Bug strong-update élément/propriété — $a[k]=v / $o->p=v sont désormais des weak writes (SsaImpl.variableWrite: isPartialUpdate → certain=false) et definitionReachingValue (DataFlowPrivate) suit uncertainWriteDefinitionInput pour que le def antérieur (teinté) traverse. Test PartialUpdate (3 BUG + 1 safe). Conséquence assumée : avec $GLOBALS field-insensitive, un write constant sur une clé ne peut plus strong-kill le taint d'une autre clé → 1 FP toléré (Globals ligne 6, annoté ; correctif précis = content par (var,clé), item B.6). Suite 47/47.

  • A.2 ☑ augmented-assign lu ($x .= …) — LHS marqué read (ancienne valeur, read-before-write) + write ; valeur du def = l'expr augmentée ; AugmentedAssignmentExpression ajouté au structuralPropagator (taint des 2 opérandes). Test AugmentedAssign (ligne 8 = le FN corrigé, + ligne 13, safe ligne 18). Suite 47/47.

  • A.3 ☑ for / foreach — ForTree/ForeachTree (PostOrderTree) avec vrais back-edges : for = init→cond→body→update→cond (φ à la condition, condition du for ajoutée aux BooleanCompletion) ; foreach = collection→header(binding)→body→header (φ au binding). Discriminant testé = taint loop-carried (use-before-assign dans le corps, FN sur CFG linéarisé). Tests ForLoopTaint/ForeachTaint.

  • A.5 ☑ Court-circuit && || ?? — LogicalAndTree/LogicalOrTree/NullCoalesceTree (PostOrderTree). &&/and : gauche vrai→droite, faux→résultat ; ||/or : gauche faux→droite, vrai→résultat (BooleanCompletion sur l'opérande gauche) ; ?? : branche non-déterministe (pas de nullness completion). Exclus du ExprTree. Taint via opérandes inchangé (structuralPropagator). Test CfgShortCircuit (expected vide = tout branche).

  • A.4a ☑ match — MatchTree (subject→bras non-déterministe→résultat, exclu de ExprTree ; MatchBlock exclu de StructuralTree pour ne pas linéariser les bras) + taint-step retour de bras → résultat (le vrai FN : le résultat ne portait aucun taint). Test MatchTaint (retours teintés flaggés, subject teinté seul = safe).

  • A.6+A.4b ☑ Complétions anormales + switch + try/catch — infrastructure complète :

    • Types : Break/Continue/Return/RaiseCompletion (classe AbnormalCompletion, non-normales) → successor-types partagés Break/Continue/Return/ExceptionSuccessor. Producteurs : AbnormalProducerTree (break/continue/return/throw évaluent leur opérande puis complètent anormalement, pas de fall-through).
    • Propagation : IfElseTree/MatchTree (et TernaryTree/TryTree) propagatesAbnormal leurs corps ; les boucles/switch attrapent break/continue et ré-émettent return/raise (loopReEmit, override last).
    • Boucles : break→sortie, continue→tête (for : →update ; do : →condition).
    • switch (SwitchTree) : subject→bras non-det, fall-through sur complétion normale, break→sortie = seule isolation (fixe le FP inter-case). continue ré-émis vers la boucle.
    • try/catch/finally (TryTree) : body→catch sur throw, finally toujours. Fixe le FN throw new E($taint) → $e->getMessage().
    • Tests batteries : LoopBreakContinue (break/continue loop-carried), SwitchTaint (fall-through=BUG, isolation break=safe, après-switch=BUG), TryCatchTaint (reflection getMessage, finally), CfgAbnormal (no-fall-through structurel), CfgCoverage (tous les constructs, 0 angle mort — lock de régression).
    • Limite v1 documentée : seul un throw explicite atteint le catch ; « tout appel peut throw » différé (FN annoté dans TryCatchTaint::through).
  • A.4c ☑ Ternaire c ? a : b / elvis c ?: b — TernaryTree (BooleanCompletion sur la condition, comme IfElseTree). Exclu de ExprTree. Taint-step des branches déjà présent.

  • CfgConsistency durci : exclut le code mort prouvable (statements après un producteur anormal) — sinon return/break bien modélisés le faisaient sur-signaler.

  • A.7 ☑ Hack uncertain-writes retiré (SsaImpl.variableWrite) : seul isPartialUpdate reste uncertain (A.1), tout autre write est certain=true ; prédicat inConditionalBranch supprimé. Le φ réel porte le taint. Test SanitizeInBranch (FP intra-branche éliminé, vrais BUG cross/single-branch conservés). Zéro régression sur toute la suite → le CFG branchant remplace complètement le hack.

✅ PHASE A COMPLÈTE : A.1–A.7 ☑. Tout le flux de contrôle PHP branche pour de vrai (if/elseif/else, while/do/for/foreach avec break/continue, switch fall-through+break, match, try/catch/finally, ternaire/elvis, court-circuit &&/||/??, return/throw), plus de hack, plus de linéarisation. CfgConsistency/CfgCoverage = 0 angle mort. Suite 57/57. Prochaine phase : B (pièces moteur stubbées) ou C (migration MAD).

Phase B — Moteur dataflow complet (retire les patches par-pattern)

  • B.1 ☐ PostUpdateNode/getPreUpdateNode généraux (receveurs + &-args, méthodes incluses) → retirer les taint-steps setter/by-ref/parse_str par-pattern. État : le step setter par-pattern (TaintTrackingPrivate ~347) couvre le cas courant $o->set($x); …$o->f (test SetterMutation vert) ; B.1 le généralise. Notes d'implémentation (vérifiées) :
    1. Nouveau variant TNode : TPostUpdateNode(Cfg::CfgNode arg) pour chaque nœud argument/receveur mutable.
    2. PostUpdateNode.getPreUpdateNode() = le nœud pré-appel (l'expression argument/receveur).
    3. Receveur comme paramètre : modéliser $this comme ParameterPosition(-1) et le receveur d'un MethodCall comme ArgumentPosition(-1) → le moteur relie $this ↔ receveur.
    4. storeStep : $this->f = $arg (dans la méthode) stocke dans le content f du PostUpdate de $this ; le PostUpdate du receveur côté appelant lit ce content → mutation propagée par le moteur.
    5. Idem pour les args par référence &$r : PostUpdate de l'argument ← écritures du param dans le corps. Puis retirer les steps setter/by-ref/parse_str hardcodés. Tests : mutation via setter (déjà SetterMutation), mutation par &-ref sur méthode, chaîne $a->setB($t); $c=$a->getB(); sink($c).
  • B.2 ◐ lambdaCreation/lambdaCall (closures + arrow-fns via $cb(...), LambdaCall) + string-callables $fn='func'; $fn(...) (getStringNamedCallee, StringCallable) + array-HO data→résultat (array_map/array_filter/… avec callback string/variable/inline, ArrayHigherOrder). Reste : call_user_func($cb, …) avec $cb variable ; puis retirer les listes HO hardcodées (Phase C MAD).
  • B.3 ☑ Named-args méthodes — le step named→param par nom marche pour fonctions ET méthodes (resolvesToCallee/calleeParam/callArgumentNode, résolution par type). Test NamedArgsMethod. (Reste : le mapping dans la couche dataflow pur — actuellement taint-step ; cf. B.3 original.)
  • B.4 ☑ viableCallable : fallback gated sur « callee typé trouvé » (not exists(getTypedCallee())), pas sur « type existe ». Test DispatchFallback (receveur typé sans méthode → fallback par nom).
  • B.5 ☑ SanitizerGuard : GuardBarrier extends Sanitizer — if (guard($x)) {…$x…} barre la lecture gardée ; noms de guards en DATA (isSanitizerGuardFunction), structure générale ; guard inconnu → chemin montré (recall-first). ctype_*/is_numeric retirés des value-sanitizers (renvoient un bool). Test GuardBarrier. v1 = branche then d'un if ; dominance (early-return/else/&&) = raffinement futur.
  • B.6 ◐ $GLOBALS['k'] cross-file = jumpStep (superglobale) → FN cross-fichier corrigé, taint-step same-file retiré. Test GlobalsSuperCrossFile. Reste : content par (classe, champ) quand type inféré ; =& reste scope-local (défendable).

Bugs moteur trouvés & corrigés hors-audit initial (via questions utilisateur sur la reachability) :

  • fix(cfg) ☑ Ordre d'évaluation des assignations : le LHS était écrit AVANT le RHS (ordre position source) → $a = f($a) (réassignation self) perdait le taint (faux cycle SSA). AssignmentTree ordonne RHS→LHS. Test TransformChain (pipeline de transforms + réassignations). FN majeur en code réel.
  • Récursivité vérifiée : chaîne interprocédurale 6-hops + dispatch méthode + return + imbrication, tout traversé (moteur shared/dataflow, pas de limite de profondeur). Test DeepChain.

Phase C — Migration DATA (le « pas de cas-par-cas », gros bloc)

  • C.1 ☐ ext/php-builtins.model.yml : migrer sources/sinks/sanitizers/steps de FlowSources.qll & TaintTrackingPrivate.qll vers MAD. Moteur = glue générique seul.
  • C.2 ☐ Migrer les listes QL des requêtes (TypeJuggling/SemgrepAudit/LaravelRoutes) vers MAD.
  • C.3 ☐ MAD au format shared upstream (colonne provenance, typeModel/neutralModel, ModelValidation).

Phase D — AST propre & complet (mergeabilité API)

  • D.1 ☐ Php::* strictement privé ; wrappers publics ; retirer les fuites de type des signatures.
  • D.2 ☐ Compléter les wrappers de nœuds manquants (include/require, shell, match, try/catch, attributs…).
  • D.3 ☐ Résolution de noms principielle (FQN calculé une fois ; table d'imports par fichier).

Phase E — Requêtes mergeable

  • E.1 ☐ Un .qhelp par requête.
  • E.2 ☐ Suites standard (code-scanning/security-extended/security-and-quality) via suite-helpers.
  • E.3 ☐ Retirer les imports internal des 5 requêtes (exposer en public).
  • E.4 ☐ Fix ReflectedXss (sink print), dédup SemgrepAudit.

Phase F — Packaging & hygiène merge (dernier, une fois le code stable)

  • F.1 ☐ php/extractor dans le workspace Cargo + Cargo.lock régénéré.
  • F.2 ☐ Cibles Bazel php/BUILD.bazel + php/extractor/BUILD.bazel + crate tree-sitter-php.
  • F.3 ☐ Décommiter le binaire extractor + dbscheme racine ; create-extractor-pack.sh standard.
  • F.4 ☐ Fix codeql-extractor.yml TRAP (none|gzip|zstd) ; overlay câblé ou retiré.
  • F.5 ☐ Relocaliser les docs sous php/docs/, restaurer README racine, sortir bench/ & AUDIT.md de la branche merge ; ajouter change-notes php.

§7 — Run de la suite de tests (baseline)

CLI CodeQL 2.25.6 dans .tooling/codeql/codeql (git-ignoré). Commande : .tooling/codeql/codeql test run php/ql/test --search-path=php --additional-packs=.

Baseline (2026-07-02, CLI 2.25.6) : All 45 tests passed. — 30 query-tests + 15 library-tests, 5m28s (extract 1m6s / comp 4m5s / eval 12.9s). Confirme que le vrai compte est 45 (docs disaient 43). C'est la référence de non-régression : tout item du plan doit finir sur 45/45 (ou +N nouveaux tests verts).

Warning de compilation à nettoyer (P0.5) : TaintTrackingPrivate.qll:349 — test is always true, as VariableAccess is a supertype… (cast redondant recv instanceof VariableAccess, recv déjà typé).

Rappel : la suite verte ne prouve PAS l'absence de bugs — l'audit a trouvé des trous masqués par omission dans les .expected (ReflectedXss print = FN réel, pourtant « vert »). Les items du plan ajoutent les tests rouges manquants avant de corriger.


§8 — Session raffinements (branche phase-b-refinements, 2026-07-03)

Après Phases D (AST public) + F (Bazel), balayage probe→fix systématique de patterns PHP modernes. 6 FN réels corrigés (chacun test-first, un commit, suite verte à chaque étape) ; suite 45 → 78 :

  • Closure capture par référence use (&$out) : write dans la closure remonte au scope englobant.
  • First-class callables (PHP 8.1) : f(...) ET $obj->m(...) modélisés comme lambdaCreation.
  • Steps DATA array/string : explode/str_split/preg_split + array_values/keys/merge/slice/… (stepModel).
  • Constructeur = setter : new C($t) avec $this->f = $param teinte $o->f — positionnel, named-args (new C(b: $t)), et property promotion (__construct(public $cmd)). Field-précis.
  • Foreach par référence foreach (… as &$v) / … as $k => &$v : la collection teinte la liaison.

Vérifié : chaque fix est dans le moteur de taint générique → s'applique à TOUS les vuln kinds (confirmé SQL, SSTI). FN niches restants documentés (non corrigés, rares) : compact('x') (résolution de var par nom dynamique) ; arrow imbriqué fn() => fn() => $x (capture transitive sur 2 niveaux).


§9 — Audit multi-agent + corrections (branche audit-fixes, 2026-07-04)

5 agents parallèles (bricolage, layering, FN, FP, AST/CFG) ont audité le pack, chaque claim vérifié par exécution de POC. Corrections livrées (test-first, suite 45→83) :

Faux positifs corrigés (angles morts introduits / latents)

  • CRITIQUE : appels méthode/statique ne passent plus en bloc leurs arguments au retour (structuralPropagator — défaisait tout sanitizer-méthode). Retour/arg décidés par l'interproc réelle ou une ligne MAD stepModel.
  • Steps cross-scope guardés par sameScope : closure use($x), $$n, =&, global $g (ce dernier exige désormais un global de fonction à au moins un bout — plus de cross-link top-level).
  • Constructeur/setter : classe résolue namespace-aware (TI::exprClass), source = argBoundToParam (per-call-site, named par nom) → plus de fuite cross-instance ni de mauvaise classe même-nom.
  • __set type-gated ; bug de précédence DataStep (toArg=-1 teintait tous les args).

Faux négatifs corrigés

  • Arrow functions fn()=>expr (ubiquitaire) : le corps est modélisé comme valeur de retour.
  • $a['k'] .= v / $this->buf .= v (augmenté sur élément/propriété) : redéfinit le conteneur + store.
  • call_user_func[_array] (string / closure / arrow) : args→params + retour→résultat.

Reste identifié, NON corrigé (documenté, MED/refactor) — pour retest

  • FN MED : self::$p/parent::$p (clé non construite) ; static FCC C::m(...) ; FCC via variable/array ; ctor déléguant à un setter ; ctor $this->data[]=$a ; [$a,&$b] destructuring by-ref ; use A\{B,C} groupé (namespace) ; new self()/static() type non inféré ; compact() ; arrow imbriqué.
  • FP recall-first ACCEPTABLE (bornés) : __toString/__set fallback type-inconnu ; static-prop partagé.
  • Layering (refactors) : migrer vers MAD les guards (ctype_*…), sanitizers-méthode, et introduire des extensibles callbackModel (HO-dispatch, triplé/incohérent aujourd'hui), outRefModel (parse_str), sanitizerGuardModel, staticmethod qualifié-classe (facade Request/Input).

§10 — Modèle inter-objets PRINCIPIEL (branche audit-fixes-med, 2026-07-04)

Objectif utilisateur : « supports tout les liens inter objets PHP », zéro bricolage, s'adapter à toute syntaxe. Remplacement du cas-par-cas par un modèle de content natif dans le moteur (comme Java/Ruby).

Moteur (DataFlowPrivate/SsaImpl) — le vrai anti-bricolage

  • Receveur $o->m() = argument -1 ; $this = paramètre -1 (ThisParameterNode) ; PostUpdateNode réel.
  • Content de champ à travers les appels, 2 jambes (trouvées par agent de debug) : base de store lisible côté callee (SsaImpl) + post-update→lecture-suivante ordonné CFG côté caller (DataFlowPrivate).
  • InitializeReturnNode : le constructeur "retourne" le $this muté comme valeur du new.
  • Fold 2-niveaux : $this->items[] = $v / $this->other->f = $v remontent d'un niveau.
  • Résolutions structurelles : new self()/static(), self::$p, list by-ref destructuring.

Bricolage SUPPRIMÉ (subsumé par le générique, vérifié sans perte)

Steps manuels setter et constructeur effacés ; helper mort argBoundToParam élagué. Gardé (recall légitime, pas bricolage) : instance-field coarse, property-promotion, magies.

Validation par agents adversariaux (2)

  • FN : setter/getter (direct/helper/hérité/trait/fluent), ctor (positionnel/nommé/promotion/factory), mutation par fonction, multi-hop, nested, arrays d'objets, collections, static props, cross-file — TOUT flowe génériquement. Les 2 seuls trous (array-append + nested à travers appel) corrigés (fold 2-niveaux).
  • FP : sweep différentiel (modèle vs pré-modèle) → AUCUN nouveau faux positif. Précision tenue : read-before-mutation (bloc/branches/boucle), confusion de champ, objet-différent, réassignation, no-store, receiver-leak, branches exclusives, cross-scope, ctor-champ-différent — tous 0. Clé sur la même déf SSA.
  • FP pré-existante corrigée : sources method par nom bare trop larges (->get()/->file()…) retirées, gardant les distinctives + le facade statique class-qualifié.

Suite : 45 → 87. Reste (schéma) : sources method qualifiées-classe ; instance-sensitivity du content (limite CodeQL fondamentale, non aggravée).


§11 — Validation sur VRAI code (DVWA / WordPress / Laravel, 2026-07-04)

Objectif : « lancer sur Laravel/WordPress sans crash ni chemin manqué », couvrir les syntaxes étranges.

Robustesse d'extraction (l'extracteur ne crash JAMAIS)

  • DVWA (169 fichiers) : extrait 100%, 0 fichier sans AST.
  • WordPress (1927 fichiers, 4.57M nœuds AST) : extrait, 0 fichier vide. 2 parse-WARN (l'extracteur avertit et continue) — limites de grammaire tree-sitter-php : function readonly() (WP compat shim) et un cas dans class-wp-block-processor. Pas des crashs.
  • Laravel framework (2973 fichiers) : extrait, 1 seul WARN = fixture de test volontairement mal formé.
  • Syntaxes inhabituelles (fichier de stress) : syntaxe alternative if:/endif, goto, heredoc/nowdoc + interpolation complexe, variable-variables, enums+match+attributes, first-class-callable, promotion, yield/yield from, anon class, traits insteadof/as, DNF types, destructuring nested/by-ref, séparateurs numériques — TOUS extraits, ZÉRO nœud ERROR tree-sitter.

Détection de vulns (ne rate pas de chemin) — DVWA

60→58 findings : Command injection (exec/ low+medium+high — les 3 réels), SQL injection ×14 (dont inter-fichier via dvwa/includes/DBMS/MySQL.php, la couche d'abstraction DB), type juggling, open redirect, XSS, path traversal, crypto key. Chemins inter-fichiers fonctionnent.

Précision sur vrai code

Le guard array-element (is_numeric($octet[0])) supprime la FP sur exec/source/impossible.php (version SÉCURISÉE de DVWA) — les 3 versions vulnérables restent détectées. Précision +, recall préservée.

Performance

Profiling XSS sur WordPress (4.5M nœuds) : goulots = CFG (208s, lib partagée, inhérent) + local-flow. Deux optimisations (résultats identiques, suite 87/87) :

  1. post-update piloté par petit ensemble + sameAccessPath non-récursif → WordPress 9m50s→3m26s (~2.9×).
  2. getContentApprox grossier (buckets par 1ère lettre du champ) pour le code OO-lourd → Laravel src 3m29s→1m15s (~2.8×). WordPress complet et Laravel src analysables en minutes. Le framework Laravel COMPLET (avec ses ~1300 fichiers de tests, mocks/closures lourds) reste lent (>8min) mais ne crash pas — en pratique on analyse le code applicatif (src+vendor), pas les tests du framework.

Limites connues (documentées)

tree-sitter-php ne parse pas function readonly() (WP) — 2/1927 fichiers partiellement extraits. Perf : le coût CFG sur très gros projets reste minutes (inhérent, comme tout pack CodeQL).

§12 — Session couverture + précision + garde-fou CI (2026-07-05)

Correction majeure : la « régression 48%→25% » était un ARTEFACT DE MESURE

SemgrepAudit.ql est taggé @tags audit → le sélecteur security-extended l'exclut. Une re-mesure database analyze de la suite seule ne comptait donc pas les ~77 positifs */security/audit/ (dont wordpress 42/42 vient à 100%). En ré-incluant l'audit : pas de régression. bench/run.sh inclut maintenant SemgrepAudit.ql, et une baseline committée (bench/baseline.txt) sert de garde-fou.

Progression rappel/précision (corpus semgrep-rules, 232 pos / 176 nég)

Étape Rappel FP
Baseline historique 113/232 (48%) 44
Fin de session 126/232 (54%) 40
Dépasse la baseline sur les DEUX axes. Suite unitaire 87→92 tests.

Faux négatifs corrigés (structurel / MAD, pas de cas-par-cas)

  • Couverture WordPress complète : tous les wrappers HTTP (wp_safe_remote_*, wp_oembed_get, vip_safe_wp_remote_get, download_url…) en SSRF, $wpdb->query, wp_delete_file, maybe_unserialize.
  • include/require — construct langage (pas un appel), donc jamais capté par la map nom→kind ; ajouté comme sink structurel « file inclusion ».
  • Array-callables [obj|class, 'method'] : résolus vers la méthode (TI::arrayCallableMethod), modélisés comme first-class-callable (lambdaCreation) + étape call_user_func. Couvre variable-held + retour.
  • new $c() (instanciation dynamique) et callback HO teinté (usort($a,$_GET['f'])) → code injection.
  • clone $x préserve le taint des champs (étape locale value-preserving).
  • Laravel : injection de NOM DE COLONNE (->orderBy/pluck/where/select($col), arg 0 — la valeur reste paramétrée/sûre).

Faux positifs corrigés (bugs réels de modèle)

  • e() (échappeur Blade) était modélisé comme SINK XSS — supprimé (c'est un sanitizer).
  • selectRaw('a = ?', [$t]) flaguait le tableau de bindings SÛR — un doublon arg -1 dans frameworks.model.yml ré-élargissait le fix arg 0 de laravel.model.yml. Doublons retirés.
  • header('X-Request-Uri: '.$x) flaguait comme open-redirect — restreint structurellement aux en-têtes Location:.

CI (garde-fou institutionnalisé)

.github/workflows/php-qltest.yml : job qltest (suite complète) + job benchmark (échoue si le rappel passe sous bench/baseline.txt). Absent auparavant ; c'est exactement ce qui aurait attrapé l'artefact de mesure ci-dessus.

Syntaxe exotique (recheck)

9.5/11 constructs PHP 8.x portent le taint (match, args nommés, nullsafe, arrow/static-arrow, heredoc, throw-expression, list() imbriqué, classe anonyme, générateur, interpolation avec appel, ternaire, [...$a]), + inter-objets (trait, factory statique chaînée, dispatch d'interface). FN résiduels (niches, documentés IMPROVEMENTS §A) : FCC de builtin, extract()/compact(), spread positionnel→variadic.

§13 — Suite session : layering complet + queries pattern (2026-07-05)

Poursuite après §12. Rappel 126→139 (54%→59%), FP 40→40, suite 92→94. Cumul session : 48%→59% rappel, 44→40 FP (dépasse la baseline sur les deux axes).

Layering MAD COMPLET (D1–D6) — « couverture en DATA, moteur en QL »

  • D1 callbackModel(name, callbackArg, dataArg) : la liste HO (array_map/usort/call_user_func…), auparavant triplée (step + 2 sinks), est une seule table extensible.
  • D2 typedSourceModel(className, method, type) : sources qualifiées-classe — $request->get() est une source seulement sur un receveur Request (ré-arme les accesseurs génériques sans FP).
  • D3 sanitizerGuardModel, D4 sanitizers-méthode, D5 outRefModel (parse_str/preg_match by-ref) : tous les noms hardcodés → MAD. D6 : frameworks.model.yml redondant supprimé (source des bugs e-sink / selectRaw arg -1).

Queries pattern PRÉCISES (pas présence-based)

Symfony CORS (Access-Control-Allow-Origin: '*' sur un *Response) + CSRF (csrf_protection => false, pas true/null, extension framework) : +13 rappel, 0 FP (symfony 0→13/18). Écrites pour éviter les pièges ok: du corpus (valeur/classe/extension exactes).

Investigué, honnêtement NON appliqué (documenté IMPROVEMENTS)

  • doctrine-dangerous-query / weak-crypto : les ok: du corpus testent la PRÉCISION (md5(...)===, $sql littéral) → une règle présence-based donne du FP (essayé doctrine : +1 FP, +0 rappel, reverté). Version précise = def-use (dbal $sql), plus gros.
  • A7 (clés de tableau) : modèle de contenu key-sensitive insuffisant seul (l'étape taint générique base→subscript, porteuse pour $_GET['x'], masque). Fix = rework moteur, risque élevé.

§14 — Queries pattern précises : 59%→70% (2026-07-05, suite)

Après le feedback du hook (« appliquer TOUT le plan »), re-examen des items reportés — dont un (le sink prepare) était reverté à tort. Vague de queries audit PRÉCISES (pas présence-based), chacune écrite pour NE PAS déclencher sur les ok: du corpus (qui testent la précision). Rappel 139→164 (59%→70%), FP 40→40, suite 89→96 tests.

Corrigé : prepare n'était pas un FP

$wpdb->prepare("…%d", $x) a un format LITTÉRAL (arg 0 non teinté) → pas flaggé ; MyDb::prepare("…" . $_GET) (concat) EST une vraie vuln → flaggé. Le test TypedSanitizer le confirme. doctrine 1→2.

Queries pattern ajoutées (toutes 0 FP)

Règle Détection précise
symfony-non-literal-redirect ->redirect(<non-littéral>) ; littéral/redirectToRoute/sans-arg = safe
symfony-permissive-cors 'Access-Control-Allow-Origin' => '*' sur *Response ; header("…: *") exact
symfony-csrf-protection-disabled 'csrf_protection' => false ; pas true/null, extension framework
weak-crypto / md5-used-as-password md5/sha1/crypt/*_file HORS opérande de comparaison (domaine type-juggling)
sha224-hash hash('sha224'/'sha512/224'/'sha3-224', …) ; sha384/512 = safe
ldap-bind-without-password ldap_bind($conn) sans argument mot de passe
php-permissive-cors header("Access-Control-Allow-Origin: *") exact (fix normStr pour double-quotes)
laravel-active-debug-code config(['app.debug'=>'true']) / putenv("APP_DEBUG=true") ; 'false' = safe

Restant (non appliqué — raisons techniques, documenté IMPROVEMENTS)

  • A7 (clés de tableau) : rendre l'étape taint base→subscript key-sensitive casserait $data=$_GET; $data['x'] (FN commun) — pire qu'une FP rare ; conversion full-content = gros rewrite, hors-session.
  • laravel-cookie/validator (13) : analyse de fichier de config ENTIER (le ruleid marque le début du tableau, le mauvais réglage est ailleurs) — pas de pattern simple sans FP.
  • base-convert (8) : flux weak-random ; openssl (6), tainted-url-host/session (7) : patterns complexes / sources non-typées. ROI décroissant, risque FP croissant.

§15 — Audit systématique de la couverture des constructs PHP du moteur (2026-07-05)

Suite à la question « le moteur supporte-t-il déjà proprement toutes les syntaxes/calls PHP ? », test systématique de ~24 constructs (source $_GET → construct → sink). 21/24 déjà OK ; 3 trous corrigés ; 2 niches documentées. Recall/FP corpus inchangés (164/40), suite 87→97.

Déjà supporté (21) — le moteur est très complet

Assignation : simple, chaînée ($a=$b=$t), composée (.=), référence ($b=&$a) + mutation par référence, null-coalesce (??=), list/destructuring (plate, imbriquée, à clé). Array : []=, lecture d'élément, spread [...$a], littéral. Portée : $GLOBALS, global, propriété statique C::$s, variables-variables $$k. Objets : readonly, clone (préserve les champs), __invoke, closures ($this, use), arrow (capture auto), enum + méthode. Flux : récursivité, static var, generators (yield keyé), exceptions (message teinté), match, ternaire. Calls : args nommés réordonnés, nullsafe, trait, factory statique chaînée, dispatch d'interface, first-class-callable.

Trous CORRIGÉS cette session (3)

Construct Avant Fix
define('C',$t); C (constante runtime) 0 étape taint define-valeur → références de la constante (globales, clé-spécifique)
$o->$m() (nom de méthode variable) 0 methodNameOf résout $m via SSA → dispatch sur la bonne méthode
f(...[$t]); $a[0] (spread → variadic) 0 calleeParamName couvre les params variadiques (avant : SimpleParameter seulement)

Niches NON corrigées (2, rares en code sécurité-sensible, documentées)

  • Fiber (PHP 8.1) : $f->start($x) passe les args au callback de la fibre — API spécifique, quasi jamais dans du code exploitable.
  • Closure::bind : rebinding de $this — rare.

§16 — Vérification couverture constructs (moteur PROD) + adaptation CMS (2026-07-06)

Découverte clé : DEUX moteurs de taint

Le pack a deux moteurs, et l'audit §15 mesurait le MAUVAIS :

  • codeql.php.dataflow.TaintTracking — v1 standalone (prédicat taintStep local), utilisé uniquement par des tests legacy, divergent.
  • codeql.php.TaintTracking — moteur PARTAGÉ de production (TaintFlowMake<…> → defaultAdditionalTaintStep), utilisé par toutes les vraies queries (CommandInjection.ql, etc.).

Un test ConstructCoverage construit sur le v1 (hasTaintFlow) montrait de faux « trous » (elvis/match/closures/arrow/spread) qui n'existent QUE dans le v1. Sur le moteur de production, les 25 constructs testés portent TOUS le taint. Nouveau test php/ql/test/query-tests/ConstructCoverage/ (config identique à CommandInjection.ql) = garde-fou permanent, 25/25.

Seul vrai trou corrigé

call_user_func('strtoupper', $t) — callee builtin (nom string, sans corps PHP) : cufCallee ne le résout pas, donc le stepModel du builtin ne s'appliquait pas via le callback. Fix : 1 disjonction dans defaultAdditionalTaintStep qui forward les args→résultat quand le callee nommé est un stepModel("function", …). Testé (ConstructCoverage #22).

Adaptation CMS = data, moteur inchangé (confirmé)

Le test library-tests/frameworks prouve déjà l'agnosticité : WordPress / Symfony / Doctrine / PrestaShop / TYPO3, chacun = un fichier ext/<cms>.model.yml (sources/sinks/sanitizers), zéro QL. Ajouter un CMS demain = un fichier data. Enrichi cette session (data pure) :

  • PrestaShop : sinks open-redirect Tools::redirect/redirectLink/redirectAdmin. Cas de test ps3.
  • WordPress : sanitizers sanitize_textarea_field/sanitize_title/wp_kses_data/esc_textarea/esc_url_raw.
  • Précision : Tools::safeOutput (XSS) volontairement PAS ajouté en sanitizer — le modèle est kind-agnostique, ça masquerait des SQLi (faux négatif).

Perf (axe 1) — conclusion

Vrai code (corpus/apps) rapide ; full-suite sur src de framework (1648 fichiers) ~15 min = inhérent (14 queries × dataflow interprocédural), PAS corrigé par les stats (prouvé : 900s avec anciennes ET nouvelles stats), PAS une régression de mes changements (HEAD 139s ≈ revert 155s). Stats régénérées depuis une DB représentative (hygiène ; recall/FP/bench inchangés).

Reste (dette)

Le moteur v1 (codeql.php.dataflow.TaintTracking) est stale/divergent — à supprimer ou resynchroniser (aucune query de prod n'en dépend ; seuls quelques library-tests). Bench inchangé : 164/232 (70%), FP 40. Suite 99/99.