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é).
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 » :
- Bricolage de soundness dans CFG/SSA — un hack
uncertain-writesqui 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). - Cas-par-cas hardcodé en QL — sources/sinks/sanitizers/steps des builtins PHP écrits en listes
de chaînes dans
.qllau 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. - Hygiène de packaging/merge — binaire extractor committé,
php/absent du workspace Cargo & Bazel, aucun.qhelp, suites non-standard, MAD sans colonneprovenance, 5 requêtes important des modulesinternal, 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.
| 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 |
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-filesle suit). Upstream ne commit jamais d'extracteur buildé. → supprimer du git,.gitignore, produire par le build.Cargo.toml:5-16—php/extractorabsent desmembersdu workspace ;Cargo.lockn'a aucune entréecodeql-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érerCargo.lock.- Aucun câblage Bazel : pas de
php/BUILD.bazelniphp/extractor/BUILD.bazel;tree-sitter-phpnon 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 quenone|gzip|zstd.brotliest inparsable,zstdinatteignable. → aligner sur l'enum.
HACK / INCOMPLET
php/build.sh+ absence dephp/scripts/create-extractor-pack.sh— script de build maison au lieu du flux standardcreate-extractor-pack.sh+ Bazel. → remplacer.php/php.dbscheme(racine) dupliquephp/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,163lisent les env_OVERLAY_*maiscodeql-extractor.ymln'a pasoverlay_support_versionniqlpack.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.
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:132resolveClassReference(Php::AstNode)— prédicat public prenant un type généréPhp::AstNodeen signature → fuite du type interne dans l'API publiée. IdemgetAUsedTrait(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 surPhp::*. 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/EnumCasedeclarations, 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,usetop-level. → wrapper systématique table-driven contreTreeSitter.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), viatoString()d'un nœud, et même fichier seulement ; les imports non-aliasés (use App\Foo;puisFoo), groupes, relatifs,\-global non gérés. → table d'imports par fichier, résolution uniforme.Callable.qll:57 getDeclaringType()retourneAstNodeviagetParent().getParent()positionnel (devrait êtreClassLike) ; idem accès positionnelsExpr.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).
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=falsemarque tout write en branche comme may-write : il ne tue jamais un def antérieur. Dans une même brancheif($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'utilisecertain=falseque pour de vrais may-writes (captures). → tous les writes syntaxiquescertain=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èsreturn/throwtraité 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.updateBaseVariablefait 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 (φ viadefinitionReachingValue) → double-modélisation, provenance ambiguë, chemins dupliqués. Retrait : supprimer le prédicat, tous les writescertain=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/TReturnCompletionjamais produits (code mort) ;returnnon modélisé comme complétion anormale.CfgConsistency.qln'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.
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), listescall_user_func/array_map. Le MAD existe déjà (ModelExtensions.qll+ext/*.ymlne portent que le framework). → livrerext/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 queisBarrier = 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émenterPostUpdateNode/getPreUpdateNodesur 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/lambdaCallgénéraux.- Named-args = taint-only + fonctions seulement.
getArgumentCfgNode:79-83retire correctement les named args du mapping positionnel, mais le step de reconnexion (:437-450) ne matche queFunctionCall→FunctionDefinition. Named args vers méthodes/statique + tout le flux pure-DataFlow = FN. → mapper named→positional dans la couche arg/parameterMatchpour 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). →getTypedCalleevide 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->idet$b->iden 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é.
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>.qhelppar requête (overview/recommendation/example/references). - Suites : seul
php-security.qls, format non-standard (hand-codeinclude: kind/tags,queries: .). Manquephp-code-scanning.qls,php-security-extended.qls,php-security-and-quality.qls. Le code-scanning ne les trouvera pas. → suites standard viasuite-helpers. - MAD = réimplémentation privée, pas le shared MaD.
ModelExtensions.qll:19-36déclare dessourceModel(3col)/sinkModel(4col)/…maison sans colonneprovenance, sansneutralModel/typeModel, sansModelValidation→ n'interopère pas avec l'outillage MaD upstream. → migrer vers les extensiblescodeql/dataflowstandard (colonnes type/path/kind/provenance) + ModelValidation. - 5 requêtes src importent
internal:SemgrepAudit,LaravelRoutes,TypeJuggling,HardcodedCryptoKey,CallResolutionCoverageimportent…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:3annoté// BUG(print($_POST['x'])) mais absent du.expected(seul leecholigne 2 y est). Trou de couverture masqué par un test vert. → ajouterprintcomme 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/LaravelRouteshardcodent 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.
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
.mddiary à la racine (PROJECT_STATUS,STRUCTURAL_ROADMAP,IMPROVEMENTS,THREAT_MODEL,PLAN,AUDIT,DEV) — racine upstream n'a rien de tel. → déplacer sousphp/docs/; exclureAUDIT.mddu merge. README.mdracine é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. → sousphp/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).
IMPROVEMENTSdit 27 puis 44. 4 chiffres différents ; vérité = 45. - while/do « à finir » (
PROJECT_STATUS:92) alors qu'implémentés +LoopTaint.expectedvert. - 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
.qldansSecurity/mais un estSemgrepAudit(helper), la liste en nomme 12. Réel : 12 sécurité + 3 utilitaires.
Vérifiés corrects : Rust 251 LOC ✓, 7 MAD ✓.
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.
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
$GLOBALScross-file. + fix ordre d'assignation$a=f($a), spreadf(...$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
internalretirés des requêtes sécurité, fix- Phase F partiel : TRAP pattern (
zstd), binaire décommité, change-notes,php/extractordans 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é (fuiteresolveClassReference(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.bazelcalqués sur ruby, ET enregistrertree-sitter-phpdans le vendoring cargo Bazel (misc/bazel/3rdparty/…viaupdate_cargo_deps.sh) — nécessite la toolchain Bazel. Le build Cargo marche déjà (dev) ; Bazel = build CI/release.- Phase F — docs : relocaliser les
.mddiary sousphp/docs/, restaurer README racine, sortirbench//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é).
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èger&d-quoting retiré.P0.2☑ NOTE mensongèreControlFlowGraphImpl.qll:244-252supprimé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.qldurci : nouvel invariantlinearisedBranch(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 redondantTaintTrackingPrivate.qll:349supprimé.
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=vsont désormais des weak writes (SsaImpl.variableWrite:isPartialUpdate → certain=false) etdefinitionReachingValue(DataFlowPrivate) suituncertainWriteDefinitionInputpour que le def antérieur (teinté) traverse. TestPartialUpdate(3 BUG + 1 safe). Conséquence assumée : avec$GLOBALSfield-insensitive, un write constant sur une clé ne peut plus strong-kill le taint d'une autre clé → 1 FP toléré (Globalsligne 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 ;AugmentedAssignmentExpressionajouté austructuralPropagator(taint des 2 opérandes). TestAugmentedAssign(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 duforajoutée auxBooleanCompletion) ;foreach= collection→header(binding)→body→header (φ au binding). Discriminant testé = taint loop-carried (use-before-assign dans le corps, FN sur CFG linéarisé). TestsForLoopTaint/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 duExprTree. Taint via opérandes inchangé (structuralPropagator). TestCfgShortCircuit(expected vide = tout branche). -
A.4a☑match—MatchTree(subject→bras non-déterministe→résultat, exclu deExprTree;MatchBlockexclu deStructuralTreepour ne pas linéariser les bras) + taint-step retour de bras → résultat (le vrai FN : le résultat ne portait aucun taint). TestMatchTaint(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(classeAbnormalCompletion, non-normales) → successor-types partagésBreak/Continue/Return/ExceptionSuccessor. Producteurs :AbnormalProducerTree(break/continue/return/throw évaluent leur opérande puis complètent anormalement, pas de fall-through). - Propagation :
IfElseTree/MatchTree(etTernaryTree/TryTree)propagatesAbnormalleurs corps ; les boucles/switch attrapent break/continue et ré-émettent return/raise (loopReEmit, overridelast). - 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).continueré-émis vers la boucle.try/catch/finally(TryTree) : body→catch surthrow,finallytoujours. Fixe le FNthrow 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
throwexplicite atteint le catch ; « tout appel peut throw » différé (FN annoté dansTryCatchTaint::through).
- Types :
-
A.4c☑ Ternairec ? a : b/ elvisc ?: b—TernaryTree(BooleanCompletion sur la condition, commeIfElseTree). Exclu deExprTree. Taint-step des branches déjà présent. -
CfgConsistencydurci : exclut le code mort prouvable (statements après un producteur anormal) — sinonreturn/breakbien modélisés le faisaient sur-signaler. -
A.7☑ Hack uncertain-writes retiré (SsaImpl.variableWrite) : seulisPartialUpdatereste uncertain (A.1), tout autre write estcertain=true; prédicatinConditionalBranchsupprimé. Le φ réel porte le taint. TestSanitizeInBranch(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).
B.1☐PostUpdateNode/getPreUpdateNodegé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(testSetterMutationvert) ; B.1 le généralise. Notes d'implémentation (vérifiées) :- Nouveau variant
TNode:TPostUpdateNode(Cfg::CfgNode arg)pour chaque nœud argument/receveur mutable. PostUpdateNode.getPreUpdateNode()= le nœud pré-appel (l'expression argument/receveur).- Receveur comme paramètre : modéliser
$thiscommeParameterPosition(-1)et le receveur d'unMethodCallcommeArgumentPosition(-1)→ le moteur relie$this↔ receveur. storeStep:$this->f = $arg(dans la méthode) stocke dans le contentfdu PostUpdate de$this; le PostUpdate du receveur côté appelant lit ce content → mutation propagée par le moteur.- 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).
- Nouveau variant
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$cbvariable ; 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). TestNamedArgsMethod. (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 ». TestDispatchFallback(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_numericretirés des value-sanitizers (renvoient un bool). TestGuardBarrier. v1 = branchethend'unif; dominance (early-return/else/&&) = raffinement futur.B.6◐$GLOBALS['k']cross-file = jumpStep (superglobale) → FN cross-fichier corrigé, taint-step same-file retiré. TestGlobalsSuperCrossFile. 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).AssignmentTreeordonne RHS→LHS. TestTransformChain(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). TestDeepChain.
C.1☐ext/php-builtins.model.yml: migrer sources/sinks/sanitizers/steps deFlowSources.qll&TaintTrackingPrivate.qllvers 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 (colonneprovenance,typeModel/neutralModel,ModelValidation).
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).
E.1☐ Un.qhelppar requête.E.2☐ Suites standard (code-scanning/security-extended/security-and-quality) via suite-helpers.E.3☐ Retirer les importsinternaldes 5 requêtes (exposer en public).E.4☐ FixReflectedXss(sinkprint), dédupSemgrepAudit.
F.1☐php/extractordans le workspace Cargo +Cargo.lockrégénéré.F.2☐ Cibles Bazelphp/BUILD.bazel+php/extractor/BUILD.bazel+ cratetree-sitter-php.F.3☐ Décommiter le binaire extractor + dbscheme racine ;create-extractor-pack.shstandard.F.4☐ Fixcodeql-extractor.ymlTRAP (none|gzip|zstd) ; overlay câblé ou retiré.F.5☐ Relocaliser les docs sousphp/docs/, restaurer README racine, sortirbench/&AUDIT.mdde la branche merge ; ajouter change-notesphp.
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.
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 commelambdaCreation. - Steps DATA array/string : explode/str_split/preg_split + array_values/keys/merge/slice/… (stepModel).
- Constructeur = setter :
new C($t)avec$this->f = $paramteinte$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).
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) :
- 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 MADstepModel. - Steps cross-scope guardés par
sameScope: closureuse($x),$$n,=&,global $g(ce dernier exige désormais unglobalde 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. __settype-gated ; bug de précédenceDataStep(toArg=-1teintait tous les args).
- 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.
- FN MED :
self::$p/parent::$p(clé non construite) ; static FCCC::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/__setfallback type-inconnu ; static-prop partagé. - Layering (refactors) : migrer vers MAD les guards (
ctype_*…), sanitizers-méthode, et introduire des extensiblescallbackModel(HO-dispatch, triplé/incohérent aujourd'hui),outRefModel(parse_str),sanitizerGuardModel,staticmethodqualifié-classe (facade Request/Input).
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).
- 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
$thismuté comme valeur dunew. - Fold 2-niveaux :
$this->items[] = $v/$this->other->f = $vremontent d'un niveau. - Résolutions structurelles :
new self()/static(),self::$p, list by-ref destructuring.
Steps manuels setter et constructeur effacés ; helper mort argBoundToParam élagué.
Gardé (recall légitime, pas bricolage) : instance-field coarse, property-promotion, magies.
- 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
methodpar 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).
Objectif : « lancer sur Laravel/WordPress sans crash ni chemin manqué », couvrir les syntaxes étranges.
- 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, traitsinsteadof/as, DNF types, destructuring nested/by-ref, séparateurs numériques — TOUS extraits, ZÉRO nœud ERROR tree-sitter.
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.
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.
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) :
- post-update piloté par petit ensemble + sameAccessPath non-récursif → WordPress 9m50s→3m26s (~2.9×).
getContentApproxgrossier (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.
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).
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.
| É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. |
- 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) + étapecall_user_func. Couvre variable-held + retour. new $c()(instanciation dynamique) et callback HO teinté (usort($a,$_GET['f'])) → code injection.clone $xpré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).
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 dansframeworks.model.ymlré-élargissait le fix arg 0 delaravel.model.yml. Doublons retirés.header('X-Request-Uri: '.$x)flaguait comme open-redirect — restreint structurellement aux en-têtesLocation:.
.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.
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.
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).
- 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 receveurRequest(ré-arme les accesseurs génériques sans FP). - D3
sanitizerGuardModel, D4 sanitizers-méthode, D5outRefModel(parse_str/preg_match by-ref) : tous les noms hardcodés → MAD. D6 :frameworks.model.ymlredondant supprimé (source des bugs e-sink / selectRaw arg -1).
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).
- doctrine-dangerous-query / weak-crypto : les
ok:du corpus testent la PRÉCISION (md5(...)===,$sqllitté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é.
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.
$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.
| 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 |
- A7 (clés de tableau) : rendre l'étape taint
base→subscriptkey-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.
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.
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.
| 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) |
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.
Le pack a deux moteurs, et l'audit §15 mesurait le MAUVAIS :
codeql.php.dataflow.TaintTracking— v1 standalone (prédicattaintSteplocal), 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.
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).
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 testps3. - 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).
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).
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.