Skip to content

Durcissement avant l'ouverture du depot en public - #18

Merged
lostmind84 merged 6 commits into
mainfrom
security/durcissement-avant-ouverture-publique
Jul 30, 2026
Merged

lostmind84 merged 6 commits into
mainfrom
security/durcissement-avant-ouverture-publique

Conversation

@lostmind84

@lostmind84 lostmind84 commented Jul 30, 2026 •

Copy link
Copy Markdown
Owner

Passe de sécurité avant l'ouverture du dépôt en public, sous AGPL-3.0. La revue a porté sur tout le dépôt et non sur un diff : secrets, historique Git, surface HTTP, chaîne de mise à jour, scripts d'installation, CI, dépendances.

Ce que la revue n'a pas trouvé, et qui vaut d'être dit : govulncheck ne signale aucune vulnérabilité atteignable, npm audit aucune, et traversée de chemin, injection SQL et XSS n'ont pas de point d'entrée. Les trois contrôles de chemin (images.go, stager.go, catalogadmin.go) sont fermés, argon2id est correctement paramétré, et le caviardage de diagnostic.zip fait les deux moitiés du travail — par nom de clé et par valeur littérale.

Les cinq commits

Commit Ce qu'il ferme
ci(deps) Les douze actions tournaient sur des tags mobiles. softprops/action-gh-release est une action tierce, dans le seul job qui reçoive contents: write — un v2 déplacé publie sous le nom de la coopérative des octets que les postes installent en LocalSystem. Épinglées sur un SHA, plus dependabot.yml pour que l'épinglage ne devienne pas une dette
feat(web) Aucune réponse ne portait d'en-tête de sécurité. CSP, nosniff, X-Frame-Options: DENY, Referrer-Policy, posés par guard avant toute décision — donc aussi sur les 403
fix(webdav) Une redirection pouvait faire tomber le TLS. La règle ne comparait que l'hôte, or net/http conserve Authorization sur une redirection vers le même hôte : le compte du partage partait en clair
docs(web) Quatre commentaires annonçaient ouvertes des routes fermées depuis ADR-033, et contredisaient un test qui exige le 401
docs SECURITY.md, l'entrée SUIVI.md, et le fichier vide $(readlink suivi à la racine

frame-ancestors 'none' est la directive qui compte

Les autres bornent ce qu'une page charge. Celle-là ferme un acte : les gestes de dépannage de §14.4 répondent sans mot de passe par décision (ADR-033), donc une page ouverte sur le poste pouvait encadrer /admin, le recouvrir, et faire cliquer « Rouleau changé » ou un auto-test à un bénévole qui croyait cliquer ailleurs.

style-src garde 'unsafe-inline' : le CSS en ligne n'exécute rien, et une politique qui casse l'écran le jour où vite change sa façon d'émettre les styles est une politique qu'on supprime à 7 h du matin au lieu de la réparer.

La CSP a été vérifiée sur les écrans réels

Pas seulement en test. Poste de démonstration lancé, catalogue déposé, les deux écrans ouverts dans un navigateur : zéro message de console, grille et 18 photos affichées, police chargée, flux SSE vivant. Et le seul motif douteux a été exercé nommément — handToBrowser (Station.svelte:421) crée un blob: par createObjectURL pour l'export de configuration, ce qu'aucune directive CSP ne couvre : téléchargement effectué, violationsCSP: [].

Trois tests Go nouveaux gardent le reste, dont un qui refuse qu'on élargisse script-src — c'est la ligne qu'on relâche pour faire remarcher un écran. Le test du WebDAV a été vérifié : il tombe sans le correctif.

Quatre écarts pesés et laissés

Le contexte est un poste peu sensible, sur le réseau d'une coopérative, devant des bénévoles. SUIVI.md porte le raisonnement de chacun ; le résumé :

  • dav.lacagette-cooperative.fr subsiste dans 9 commits de main (943a961→776f184, corrigé par 69e2f11) — la réécriture de docs/00-donnees-retirees.md n'avait couvert que les deux premiers commits. Le domaine n'existe plus et le mot de passe qui l'accompagne est un faux ; réécrire l'historique coûterait plus que le risque.
  • install-sheet.txt reste en clair dans DataRoot, lisible par le compte du kiosque — mot de passe Windows et code de secours.
  • Le compte du kiosque a Modify sur data\updates, où LocalSystem exécute ensuite update.ps1. Élévation locale réelle, mais elle demande un clavier sur le poste.
  • /admin/api/health publie l'URL WebDAV et le compte sans mot de passe, là où internal/diag/redact.go les caviarde de l'archive.
  • L'export CSV du journal n'échappe pas les cellules commençant par =, +, -, @.

Écrits nulle part, ils seraient re-signalés dans six mois — ou « corrigés » par quelqu'un qui ne saurait pas qu'ils ont été décidés.

Examinés puis écartés sans code, et désormais dans SECURITY.md : signer les releases (une clé privée à tenir des années par une équipe bénévole, et une clé perdue bloque tout le parc), servir le poste en HTTPS, fermer les routes ouvertes d'ADR-033. Un fait vérifié plutôt que supposé les renforce : la table weighings ne porte aucun identifiant de client, donc son export ouvert n'expose pas de donnée personnelle.

Vérification

go vet ./...                      vert
gofmt -l .                        rien
go test -race -short (CGO=1)      31 paquets, 0 échec
go test -short (CGO=0)            31 paquets, 0 échec
boundary                          les coupes sont respectées
deps                              6 dépendances directes, conformes à §17.1
internal/web/dist                 inchangé

Une observation restée ouverte. Sur douze exécutions de la suite complète, une a signalé un échec dans un paquet, jamais reproduit ensuite — et la commande de comptage qui l'a vu n'en a pas gardé le nom. Ce n'est pas imputable à cette branche : les trois tests nouveaux ont tenu 50 exécutions chacun, et internal/web dix passes sous -race. ci.yml documente déjà une famille de tests sensibles au planificateur (skipUnderShort) — première piste si le symptôme revient.

Le compteur d'en-tête de SUIVI.md n'est pas remesuré ici : la méthode qui a produit « 2 572 » n'est pas celle qui donne 2 573 PASS / 7 SKIP sur une passe verte, et remplacer un chiffre par l'autre aurait prétendu à une continuité fausse.

Reste à faire à la main, hors dépôt

Dans les réglages GitHub, avant la bascule en public : Secret scanning et Push protection. dependabot.yml s'active seul.

Un tag d'action GitHub n'est pas une version, c'est un pointeur : son
proprietaire le deplace quand il veut, et une action deplacee s'execute dans nos
runners avec ce que porte son depot a cet instant-la. Le depot part en public, et
c'est le chemin d'attaque le moins couteux qui existe contre une chaine de
construction : personne n'a besoin d'un acces a ce depot-ci pour l'emprunter.

softprops/action-gh-release est le cas qui compte. C'est une action TIERCE, et
elle tourne dans le seul job du depot qui recoive contents: write et le
github.token -- celui qui publie la page Releases dont les postes tirent ensuite
leur mise a jour. Un v2 deplace publie sous le nom de la cooperative des octets
que internal/update installera en LocalSystem, et le condensat ne dirait rien :
SHA256SUMS-archives.txt est produit par le meme workflow.

Les quatre actions sont donc epinglees sur un SHA de commit, qui designe des
octets qui ne changent plus. Le tag lisible reste en commentaire au bout de la
ligne : il est la pour l'humain qui relit, et c'est la seule chose que le SHA ne
dit pas. Les majeures ne bougent pas -- setup-node reste en v6 et
action-gh-release en v2 -- parce qu'epingler et monter de version sont deux
gestes, et qu'un seul etait demande.

dependabot.yml arrive DANS LE MEME COMMIT, et ce n'est pas un ajout de confort.
Epingler protege la chaine et fige aussi les correctifs des actions : sans une
entree github-actions qui fait monter le SHA et son commentaire ensemble,
l'epinglage devient une dette que personne ne relit. Les deux tiennent ensemble
ou aucun des deux ne tient.

La cadence est mensuelle et les demandes sont groupees par ecosysteme. Une
cooperative n'a pas de mainteneur a temps plein : une demande par dependance et
par semaine, c'est un depot ou l'on cesse de regarder les demandes, donc un depot
ou la mise a jour de securite passe inapercue avec les autres.

L'entree npm porte un avertissement pour qui relira ces demandes.
internal/web/dist est commite et le job front compare ses OCTETS au resultat de
npm ci && npm run build : une montee de vite ou de svelte change les empreintes
des noms de fichiers, donc l'etape « internal/web/dist est a jour » devient rouge
sur une demande par ailleurs correcte. La reponse est celle que le workflow ecrit
deja -- lancer make front et committer le dist dans la demande -- jamais retirer
la garde, qui a rattrape deux livraisons d'un ecran perime.
Quatre en-tetes, poses par guard -- le middleware le plus externe -- avant toute
decision et avant tout handler. Le refus est le cas qui decide de l'endroit : un
403 de l'Origine est une reponse comme une autre, et des en-tetes poses apres
avoir tranche laisseraient nue la seule reponse qu'un attaquant obtient.

frame-ancestors 'none' est la directive qui compte ici, et c'est la seule qui
ferme un ACTE plutot qu'un chargement. Les gestes de depannage de §14.4
repondent sans mot de passe par decision (ADR-033), donc une page ouverte sur le
poste pouvait encadrer /admin, le recouvrir, et faire cliquer « Rouleau change »
ou un auto-test a un benevole qui croyait cliquer ailleurs. X-Frame-Options: DENY
suit, redondant sur tout navigateur qu'un poste fait tourner, garde parce qu'il
coute une ligne et couvre le navigateur que personne n'a choisi.

nosniff est l'autre moitie d'un controle qui existait deja. La route des images
refuse de servir un PNG sous un nom en .jpg (§10.7) -- dix des 181 photos reelles
sont dans ce cas -- et un navigateur qui ignorerait le type declare pour renifler
les octets defaisait ce controle depuis l'exterieur.

script-src 'self' est verifiable sur les fichiers construits et l'a ete : les
bundles ne contiennent ni eval, ni new Function, ni insertRule, et les deux pages
d'entree ne portent aucun script en ligne. img-src 'self' sans data: est
deliberé : le CSV porte ses images en base64, mais l'import les decode et les
ecrit par adresse de contenu, donc un data: sur cet ecran signalerait un chemin
qui a saute cette etape.

style-src garde 'unsafe-inline', et c'est une decision et non un oubli. Le CSS en
ligne n'execute rien, et le retirer couterait la page de secours d'un binaire
construit sans front, qui porte sa mise en forme dans un attribut style. Une
politique qui casse l'ecran le jour ou vite change sa facon d'emettre les styles
est une politique qu'on supprime a 7 h du matin au lieu de la reparer.

Pas de HSTS et pas d'upgrade-insecure-requests : le service parle HTTP en clair
sur la boucle locale, et les deux epingleraient ou reecriraient une adresse qu'un
benevole ne pourrait alors plus atteindre du tout.

LA POLITIQUE A ETE VERIFIEE SUR LES DEUX ECRANS REELS, pas seulement en test.
Poste de demonstration lance, catalogue depose, les deux ecrans ouverts dans un
navigateur : zero message de console, grille et photos affichees, police chargee,
flux SSE vivant. Le seul motif douteux a ete exerce nommement -- handToBrowser
(Station.svelte) cree un blob: par createObjectURL pour l'export de
configuration, ce qu'aucune directive CSP ne couvre : le telechargement passe,
sans violation.

Deux tests gardent le reste, et le second refuse qu'on elargisse script-src :
c'est la ligne qu'on relache pour faire remarcher un ecran.
…e partait en clair

La regle de redirection de §10.1 refusait tout saut hors de l'hote declare, et
elle ne regardait que l'HOTE. Or net/http conserve l'en-tete Authorization sur
une redirection vers le MEME hote -- ce que le controle laissait passer par
construction -- si bien qu'un partage repondant « 302 http://meme-hote/... » a une
requete https mettait username:password sur le fil, en clair, sur un reseau que
quelqu'un croyait protege par TLS.

Le trou ressemble a un controle ferme, et c'est ce qui le rend interessant : la
verification existait, elle etait juste, et elle ne couvrait pas ce cas. Rien
dans le symptome ne l'aurait dit non plus -- le catalogue arrive, le poste le
sert, la pastille est verte. Aucun bruit, et le compte du producteur lisible par
qui ecoute le reseau du magasin.

newClient recoit donc le SCHEMA en plus de l'hote, tous deux tels que
catalog.options.url les declare, et refuse un saut de https vers autre chose. Il
les recoit plutot que de lire via[0] parce que la valeur declaree est deja ce que
cette fonction prend en argument pour l'hote : une regle qui compare a ce qu'un
exploitant a tape ne depend alors d'aucun contrat de net/http.

Le schema declare est un PLANCHER et non une preference : http vers https reste
suivi, et le test le verifie explicitement. Refuser une montee en TLS aurait
casse un partage mal configure sans rien proteger, et le sens de la regle est que
le compte ne descende jamais, pas que l'adresse ne bouge jamais.

Le test exerce la regle a travers le CheckRedirect du client et non a travers un
serveur : reproduire le saut de bout en bout demanderait un serveur TLS de test
et son certificat auto-signe injecte dans un transport que ce paquet ne laisse
deliberement personne configurer -- beaucoup d'echafaudage pour observer une
comparaison. Il a ete verifie qu'il tombe sans le correctif.
…nt fermees

« It is NOT authenticated, and it is the one route of ADR-018 where that deserves
a sentence » : c'est ce que dit encore importCatalog, et c'est faux depuis
qu'ADR-033 a deplace le critere. La table des routes de server.go garde
manual-entry et catalog/import derriere le mot de passe, et un test de
session_test.go exige le 401 sur les deux -- les commentaires contredisaient donc
un test qui passe.

C'est un defaut plus ennuyeux qu'une faute de frappe. Un relecteur qui decide sur
la foi d'un commentaire -- « cette route est ouverte, donc le garde n'a rien a
voir avec elle » -- se trompe, et il se trompe sur la seule question que le
fichier existe pour trancher. Quatre endroits le disaient :

  - importCatalog et manualEntry, dans internal/web/troubleshooting.go ;
  - l'interface Troubleshooting de server.go, « the unauthenticated buttons of
    §14.4 », alors qu'un de ses trois membres ne l'est plus ;
  - adminTroubleshooting de cmd/openscale/admin.go, « the three unauthenticated
    switches », le meme mensonge un etage plus bas.

Les quatre disent maintenant lequel des trois est ferme, et surtout POURQUOI le
raisonnement d'ADR-018 ne suffisait plus : « n'ecrit pas la configuration »
n'a jamais ete ce qui rendait ces gestes anodins. La saisie manuelle coupe la
balance et laisse le CLIENT taper son propre poids ; un depot de catalogue
remplace toute la grille par un fichier que quelqu'un a apporte. Les deux
laissent leur trace a la caisse, et c'est plus lourd que ce que le mot de passe
gardait avant eux.

Ce qui reste vrai est garde : les gardes de §10.4 protegent le catalogue en
service, pas un mot de passe. Un mot de passe ne rend pas sur un export ampute,
il nomme seulement qui l'a depose.

Aucun changement de comportement -- quatre commentaires, et la table des routes
reste seule a decider.
Un depot public est lu par d'autres yeux que ceux d'une cooperative, et il recoit
des rapports. SECURITY.md dit ou signaler -- le formulaire prive de GitHub, jamais
une issue publique -- et ce qu'une equipe benevole peut promettre : accuse de
reception sous sept jours, pas de programme de recompense, et des correctifs sur
la derniere version publiee seulement, parce qu'il n'y a pas de branche de
maintenance pour un parc de quatre postes.

Le reste du fichier est le MODELE DE MENACE ASSUME, et c'est la sa valeur.
L'acces physique vaut l'acces administrateur (§15.2), les gestes de depannage
repondent sans mot de passe par decision (ADR-033), le service parle HTTP en clair
sur la boucle locale, le symbole EAN-13 de l'etiquette est tronque par compromis
de geometrie, et les mises a jour sont verifiees par condensat et non signees.
Sans ces paragraphes, on repond trois fois au meme rapport ; avec eux, celui qui
allait l'ecrire trouve la reponse avant.

Il dit aussi ce qui EST dans le perimetre, parce qu'une liste de non-failles sans
son complement decourage les rapports utiles : tout ce qui s'exploite sans etre
devant le poste -- depuis une page ouverte sur la machine, depuis le reseau du
magasin, depuis un CSV que le producteur depose, depuis une reponse du partage,
depuis une release -- et l'elevation du compte du kiosque vers LocalSystem.

L'entree de SUIVI.md porte les quatre correctifs de cette branche ET LES QUATRE
ECARTS LAISSES, avec le raisonnement de chacun. C'est cette seconde liste qui
justifie l'entree : l'hote reel qui subsiste dans neuf commits de l'historique, la
fiche d'installation en clair sur le poste, l'ACL du compte du kiosque sur
data\updates, l'URL WebDAV publiee par /admin/api/health, et l'export CSV qui
n'echappe pas ses formules. Ils ont ete peses -- poste peu sensible, reseau d'une
cooperative, benevoles -- et non ignores. Ecrits nulle part, ils seraient
re-signales dans six mois, ou « corriges » par quelqu'un qui ne saurait pas
qu'ils ont ete decides.

Deux choses y sont dites honnetement plutot que passees. Une execution de la suite
complete sur douze a signale un echec jamais reproduit ensuite, et la commande de
comptage qui l'a vu n'en a pas garde le nom ; ce n'est pas imputable a cette
branche -- les trois tests nouveaux ont tenu cinquante executions chacun, et
internal/web dix passes sous -race. Et le compteur d'en-tete n'est PAS remesure
ici : la methode qui a produit « 2 572 » n'est pas celle qui donne 2 573 PASS et
7 SKIP sur une passe verte, et remplacer un chiffre par l'autre aurait pretendu a
une continuite fausse.

Le fichier « $(readlink », vide et suivi a la racine, part avec ce commit. C'est
le residu d'une commande shell ratee, et c'est la premiere ligne que voit qui
ouvre le depot.
@lostmind84
lostmind84 force-pushed the security/durcissement-avant-ouverture-publique branch from 99a5a3d to c6f4e4d Compare July 30, 2026 10:13
…aque commit

Le depot part en public sous AGPL-3.0 et il est destine a circuler entre
cooperatives. Rien n'y a a renvoyer vers une conversation privee, et le plan de la
politique de dependances faisait exactement le contraire : sa consigne de style
demandait de terminer chaque message par un lien de session d'assistant, et ses
sept messages d'exemple le portaient pour montrer comment faire.

C'est la SOURCE et pas seulement une occurrence. Une consigne ecrite dans un plan
du depot est lue et appliquee par qui reprend le travail : les 62 commits de main
qui portent un tel lien viennent de la, et sans ce commit le motif se
reproduirait au plan suivant.

La consigne reste -- Conventional Commits, sujet en francais sans accents, corps
accentue, c'est bien la convention du depot -- et elle dit maintenant l'inverse
sur ce point : aucun lien de session ni mention d'outil en pied de message. Les
sept pieds des messages d'exemple partent avec elle, la ligne vide qui les
precedait comprise, si bien que les exemples montrent desormais ce qu'ils
prescrivent.

Le controle tient en une ligne, et il doit rester sans reponse :

    git log -1 --format=%B | grep -i assistant-de-session

Rien d'autre ne change dans le plan : ni une tache, ni un critere, ni un chiffre.
@lostmind84
lostmind84 force-pushed the security/durcissement-avant-ouverture-publique branch from 43f399b to 99b898b Compare July 30, 2026 10:17
@lostmind84
lostmind84 merged commit c756c31 into main Jul 30, 2026
6 checks passed
@lostmind84
lostmind84 deleted the security/durcissement-avant-ouverture-publique branch July 30, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant