Haarvisie verslagen
← Alle verslagen📄 Download als PDF
🎧 Luister dit verslag
1,0x
⬇️ Download naar telefoon
📖 Lees mee met de podcast

Hoi Jamal, welkom bij deze speciale update. We hebben een flinke security-audit achter de rug, en ik wil je graag meenemen door de belangrijkste bevindingen. Het is een stevig stuk werk geweest, maar de inzichten zijn cruciaal voor de veiligheid en stabiliteit van onze systemen.

Ons team heeft diep gegraafd. Zestien agents hebben maar liefst 320 bestanden in vijf van onze repositories doorgespit. Elke potentiële kwetsbaarheid is daarna nog eens kritisch bekeken door een aparte scepticus – iemand met de opdracht om het onderuit te halen. En wat er overbleef, Jamal? 96 échte problemen. Van de 104 meldingen die we vonden, zijn er 8 afgevallen omdat ze niet standhielden bij de controle, wat betekent dat de 96 die overbleven keihard zijn bevestigd.

De allerergste bevindingen springen er meteen uit: we hebben geheime sleutels en tokens die gewoon in onze Git-repositories staan. Iedereen met toegang tot die repo’s, zelfs met een oude kopie, kan deze geheimen misbruiken. En, minstens zo zorgwekkend: onze eigen beveiligingsscanner doet echte inlogpogingen op onze live websites, wat serieuze risico’s met zich meebrengt zoals het blokkeren van IP-adressen of zelfs accounts.

Laten we de belangrijkste aandachtspunten even doornemen:

Allereerst die geheimen. Een toegangssleutel voor het gedeelde brein staat in de backup-repo van de Lenovo-bot. Zelfs als je die nu weghaalt, blijft hij in de Git-geschiedenis staan. Dit moet je direct intrekken, vervangen, en voortaan via een omgevingsvariabele laden. Ook staat er een GitHub-token in platte tekst in de security-repo van Harvisie, direct leesbaar in de remote-URL. Dat token moet je intrekken, opnieuw aanmaken en via een veiliger methode instellen.

Dan die scanner die op onze live sites brute-force inlogpogingen doet, bijvoorbeeld op haarvisie.nl. Bij elke scan worden er foute logins naar WordPress gestuurd, wat je live site kan platleggen of je eigen IP kan blokkeren. De oplossing is simpel: dit alleen op een testomgeving draaien of de actieve inlogpogingen helemaal weghalen.

Verder hebben we gezien dat een rapport met al je zwakke plekken, ‘laatste-rapport.md’, in Git terecht is gekomen en zo in de geschiedenis van de Harvisie security-agent repo staat. Dit bevat gevoelige informatie over openstaande paden en versies. Dit moet uit Git worden gehaald en voortaan extern worden opgeslagen.

Over naar de website. Op het afspraakformulier kan de klant op de laatste stap klikken op blokken die verwijzen naar stappen die niet meer bestaan. De pagina wordt dan blanco, en de hele aanvraag is weg. Dit moet leiden naar de juiste stap, of in ieder geval niet tot een lege pagina. Ook de opmaak van het afspraakformulier werkt niet goed. Kleuren, hoeken en lettertypes worden niet toegepast omdat de CSS-code zoekt op een ID terwijl de HTML-pagina een CLASS gebruikt. Dit is een simpele fix van de verwijzing.

Bij de Lenovo-bot zien we dat deze opdrachten uit de wachtrij blind uitvoert als shell-commando's, zonder te controleren wie ze stuurt. Dit is een gevaarlijke kwetsbaarheid die misbruikt kan worden om willekeurige commando’s uit te voeren. Daarnaast grijpt de Lenovo-bot in de wachtrijen van andere bots, door taken die langer dan tien minuten duren terug te zetten. Dit leidt tot dubbel werk. En de queue-worker vinkt opdrachten af *voordat* ze zijn uitgevoerd, waardoor vastgelopen taken stil verdwijnen en nooit meer worden opgepakt.

Nog een punt van aandacht bij de Lenovo-bot: een concept-mail met ‘bounce’ in het onderwerp kan ongemerkt een externe ontvanger krijgen, zelfs als er geen oud adres is. Interne notities kunnen zo per ongeluk naar buiten lekken.

De video-tool heeft ook wat issues. De beatdetectie van de video-tool werkt bij geen enkel bestand omdat de audio via een pijp wordt gelezen, en dan klopt de lengte in de bestandskop niet. Dit moet worden opgelost door de lengte correct uit de buffer te halen. Erger nog, er zijn drie plekken in de video-tool waar vreemde tekst, zoals menu-namen, als code kan worden uitgevoerd, wat een groot risico op code-injectie met zich meebrengt. Dit vereist grondig ontsnappen van tekst en een veiliger beheer van de ‘bridge’ component.

Wat het gedeelde brein, gbrain, betreft: de kostenrem werkt niet als je bijvoorbeeld `--max-usd=5` met een isgelijkteken schrijft. De vlag wordt dan genegeerd en je draait onbeperkt dure API-calls. Dit moet een gezamenlijke lezer krijgen voor beide schrijfwijzen. De gezondheidscheck van gbrain liegt ook: hoe meer er kapot is, hoe gezonder het rapport eruitziet, omdat fouten stil worden weggegooid. Elke controle moet een waarschuwing geven bij een fout.

De webhook van gbrain schrijft buiten de beveiligde poort om, wat betekent dat externe partijen pagina’s onzichtbaar kunnen maken en de bron van schrijftoegang wordt genegeerd. Dit moet in lijn worden gebracht met de reguliere schrijfroute. Ook werken drie bestandsfuncties van gbrain niet op een lokaal brein omdat ze de oude vaste databaseverbinding gebruiken in plaats van de meegegeven engine. En als laatste voor gbrain: er zijn twee bijna identieke database-motoren van 6000 regels die uit de pas lopen. Dit leidt tot inconsistenties, zoals filters die in de ene wel werken en in de andere niet. Dit moet worden opgelost met een gedeelde basisklasse en tests om consistentie te waarborgen.

Als laatste hebben we gezien dat een timeout in gbrain taken doodt die nog maar net gestart zijn, omdat de bewaking de looptijd vanaf de allereerste poging berekent, inclusief alle wachttijd. Dit voorkomt dat een taak zijn volle looptijd krijgt.

Een hele lijst, ik weet het, Jamal. Maar deze audit geeft ons een kristalhelder beeld van waar we direct actie moeten ondernemen om onze systemen robuuster en veiliger te maken. De fixes zijn helder en uitvoerbaar, en we staan klaar om je hierbij te ondersteunen. Laten we hier snel over in gesprek gaan om de prioriteiten te bepalen en deze punten aan te pakken. Bedankt voor je aandacht.

Zestien agents lazen 320 bestanden in vijf repo's. Elke melding is daarna door een
aparte scepticus nagetrokken, met de opdracht hem onderuit te halen. Wat je hieronder
leest, is wat die controle overleefde: 96 van de 104. De 8 die afvielen staan onderaan.

96 echte problemen bevestigd, verdeeld over 5 repo's, na het lezen van 320 bestanden.

8 meldingen vielen af bij de controle, omdat ze bij natrekken niet klopten.

Het ergste: er staan twee geheimen in git (een sleutel voor het gedeelde brein en een GitHub-token), en de beveiligingsscanner doet echte inlogpogingen op je live websites.


Wat je nu moet doen

1. Sleutel voor het gedeelde brein staat in git

Wat gaat mis: in de backup-repo van de Lenovo-bot staat een toegangssleutel voor de gbrain-server gewoon in het bestand. Iedereen die die repo kan lezen, of een oude kopie heeft, kan het gedeelde brein uitlezen en beschrijven. Ook als je de regel nu weghaalt, blijft de sleutel in de git-geschiedenis staan.

Waar: lenovo-bot/.mcp.json, regel 11. Type geheim: een bearer-token voor de gbrain-API.

Fix: sleutel intrekken en vervangen. Bestand uit git halen en in .gitignore zetten. Sleutel via een omgevingsvariabele laden. Git-geschiedenis opschonen.

2. GitHub-token in platte tekst in de security-repo

Wat gaat mis: in haarvisie-security-agent/.git/config staat een GitHub personal access token gewoon leesbaar in de remote-URL. Dit is niet gemeld door de scanner zelf, het kwam bij de controle boven water.

Waar: .git/config van de repo haarvisie-security-agent.

Fix: token intrekken op GitHub, opnieuw aanmaken, en de remote instellen via de credential helper of SSH in plaats van in de URL.

3. De scanner doet echte inlogpogingen op haarvisie.nl en haartips.nl

Wat gaat mis: bij elke scan worden er drie foute logins verstuurd naar wp-login.php van je live sites, met gebruikersnaam admin. Dat is een echte brute-force-poging, geen passieve check. Een beveiligingsplugin of fail2ban kan daardoor jouw eigen IP blokkeren of het admin-account op slot zetten. Het rapport van 28-7 bewijst dat dit echt is uitgevoerd op beide sites. Extra bezwaar: haarvisie.nl mag volgens het regelboek alleen gelezen worden.

Waar: checkers/external/wordpress_checker.py, regel 51 tot 57.

Fix: de actieve inlogpogingen weghalen. Alleen passief kijken, of dit uitsluitend op een testomgeving draaien.

4. Het rapport met al je zwakke plekken staat in git

Wat gaat mis: laatste-rapport.md staat wel in .gitignore, maar was al eerder toegevoegd aan git. Daardoor gaat hij bij elke commit gewoon mee. In dat rapport staat welke paden op je sites open staan, welke WordPress-versie draait en welke beveiligingsheaders ontbreken. Er staat al zo'n rapport in de geschiedenis van twee commits.

Waar: laatste-rapport.md, repo haarvisie-security-agent.

Fix: git rm --cached laatste-rapport.md, geschiedenis opschonen, en rapporten voortaan buiten de repo wegschrijven.

5. Klant tikt op het overzicht en het formulier wordt blanco

Wat gaat mis: op de laatste stap van het afspraakformulier kan de klant op een blok tikken om iets te wijzigen. Twee van die blokken verwijzen naar stap 2 en stap 3. Die stappen bestaan sinds de samenvoeging van 20-7 niet meer. De code verbergt dan alle stappen en vindt niets om te tonen. De klant ziet een lege pagina met "Stap 0 van 6" en geen enkele knop. Alleen verversen helpt, en de hele aanvraag is weg.

Waar: app/public/afspraak-logica.js, regel 801 en 810.

Fix: die twee blokken naar stap 1 laten verwijzen. En als vangnet: als het doelscherm niet bestaat, niets verbergen en blijven staan.

6. De opmaak van het afspraakformulier werkt niet

Wat gaat mis: alle kleuren, hoeken en lettertypes van het formulier hangen aan een naam die nergens in de pagina voorkomt. De code zoekt een id, de pagina heeft alleen een class. Gevolg: de keuzeknoppen krijgen geen witte achtergrond maar worden doorzichtig op de creme pagina. Hetzelfde geldt voor de grijze tekstkleur (24 keer gebruikt), de afgeronde hoeken (7 keer) en het lettertype (7 keer). Dit is nagemeten in de gebouwde versie: 17 treffers op de verkeerde naam.

Waar: app/src/afspraak-form.css, regel 10.

Fix: overal de id-verwijzing vervangen door de class, of de kleuren en fonts één keer bovenaan de site zetten.

7. De Lenovo-bot voert opdrachten uit de wachtrij blind uit

Wat gaat mis: tekst uit de wachtrij gaat letterlijk als opdracht naar Claude, die draait met alle toestemmingen vooraf goedgekeurd. Telegram is wel afgeschermd op jouw chat, de wachtrij niet. Er is geen lijst van wie er mag sturen, en de tekst wordt niet gemarkeerd als "dit is data, geen bevel". Stuurt een andere bot de tekst van een binnengekomen mail door, en staat daar toevallig "verwijder map X" of "stuur dit adres een mail" in, dan voert Lenovo dat gewoon uit.

Waar: lenovo-bot/lenovo-bot.js, regel 186 tot 190, met de instelling op regel 77.

Fix: alleen opdrachten van bekende bots accepteren, de opdrachttekst inpakken in tags met de uitleg dat het data is, en de "sla alle toestemmingen over"-vlag vervangen door een beperkte lijst toegestane acties.

8. Lenovo zet opdrachten van andere bots terug op de rij

Wat gaat mis: de opruimactie kijkt alleen of een opdracht langer dan 10 minuten in behandeling is. Er wordt niet gekeken van wie die opdracht is. Lenovo grijpt dus ook in de rijen van Sjakie, Coco, Paco en Conductor. Duurt een klus van Sjakie 25 minuten, dan zet Lenovo hem na 10 minuten terug en pakt een tweede bot dezelfde klus op. De klus wordt dan twee keer gedaan, bijvoorbeeld twee keer dezelfde concept-mail.

Waar: lenovo-bot/lenovo-bot.js, regel 158.

Fix: twee filters toevoegen zodat alleen de eigen claims worden teruggezet.

9. Opdrachten verdwijnen stil bij de queue-worker

Wat gaat mis: de rij wordt op "klaar" gezet vóórdat het werk begint. Loopt de opdracht daarna vast of tegen de tijdslimiet van 60 minuten aan, dan staat hij al afgevinkt. Niemand pakt hem nog op en er komt geen antwoord terug. De opdracht is gewoon weg.

Waar: lenovo-bot/queue-worker.js, regel 199 (het werk start pas op regel 223).

Fix: hetzelfde claim-patroon gebruiken als in lenovo-bot.js: eerst op "in behandeling", pas na een gelukt antwoord op "klaar".

10. Concept met "bounce" in het onderwerp krijgt stiekem een externe ontvanger

Wat gaat mis: het script zoekt op "oud adres in de aan-regel OF het woord bounce in het onderwerp". Bij een treffer op alleen het onderwerp wordt er niets verwijderd, maar wel een externe ontvanger toegevoegd, en het concept wordt opgeslagen. Ligt er een interne notitie met onderwerp "Bounce-analyse voor Jamal", dan krijgt die er een externe ontvanger bij. Verstuur je dat concept later, dan gaat interne informatie naar buiten.

Waar: lenovo-bot/update-draft-gerrie.py, regel 35.

Fix: alleen matchen op het oude adres in de ontvangers, en niets opslaan als er geen ontvanger verwijderd is.

11. De beatdetectie van de video-tool werkt bij geen enkel bestand

Wat gaat mis: de audio wordt door een pijp gelezen, en dan klopt de lengte in de bestandskop niet. De code vertrouwt op die lengte en botst daardoor altijd. Zelf nagedraaid op een gewoon geluidsbestand van 3 seconden: foutmelding. Dit is de kernfunctie voor muziekvideo-montage en hij faalt altijd.

Waar: resolve_pilot/analyzers/beats.py, regel 43.

Fix: het aantal samples uit de echte buffer rekenen, of eerst naar een tijdelijk bestand decoderen in plaats van naar een pijp.

12. Drie plekken in de video-tool waar vreemde tekst code kan worden

Wat gaat mis: menu-namen, bestandsnamen en notities worden zonder controle in scripts geplakt. Wie die tekst aanlevert, kan het script afsluiten en eigen regels toevoegen, die vervolgens shell-commando's kunnen draaien. Getest en bevestigd: een geprepareerde menunaam levert een geldig script op met een shell-commando erin. Ook de Lua-fragmenten die jij zelf in de Resolve-console plakt, kunnen zo code bevatten die je niet ziet.

Waar: resolve_pilot/ui_automation/applescript.py regel 125, resolve_pilot/lua_snippets/library.py regel 53, en resolve_pilot/lua_snippets/bridge.lua (die voert 30 minuten lang elk bestand uit dat in /tmp verschijnt, zonder enige controle).

Fix: alle tekst netjes ontsnappen voor hij in een script gaat. De bridge naar een map in je eigen home met beperkte rechten verplaatsen, met een gedeeld wachtwoord erbij. Let op: de bridge wordt nergens door de tool aangeroepen, dus die kun je ook gewoon weggooien.

13. De kostenrem van gbrain werkt niet bij de gebruikelijke schrijfwijze

Wat gaat mis: schrijf je --max-usd=5 met een isgelijkteken, dan wordt de vlag niet herkend en staat de uitgavenrem helemaal uit. Geen waarschuwing, geen foutmelding. Draait dat in een geplande taak, dan loopt hij onbeperkt door op betaalde API-calls. Hetzelfde geldt voor --target-score= en --max-jobs=. In 21 bestanden zit zo'n eigen vlaggenlezer die alleen de spatie-vorm kent.

Waar: src/commands/doctor.ts, regel 7934.

Fix: één gedeelde lezer die beide schrijfwijzen aankan, en die hard stopt bij een onbekende waarde in plaats van stil door te gaan.

14. De gezondheidscheck van gbrain liegt als er iets kapot is

Wat gaat mis: de check bestaat uit één functie van ruim 3000 regels met 193 losse controles en 73 plekken waar een fout stil wordt weggegooid. Struikelt een controle, bijvoorbeeld omdat een tabel ontbreekt, dan verdwijnt hij uit de lijst. Hij kan dan ook geen strafpunten meer opleveren. Hoe meer er kapot is, hoe gezonder het rapport eruitziet.

Waar: src/commands/doctor.ts, regel 4161.

Fix: elke controle door één runner laten lopen die bij een fout een waarschuwing toevoegt in plaats van niets.

15. De webhook van gbrain schrijft buiten de beveiligde poort om

Wat gaat mis: de route POST /ingest leest de bestemming uit de kopregels van het verzoek en schrijft rechtstreeks weg. Twee dingen gaan daardoor mis. Eén: markeringen die alleen door vertrouwde schrijvers gezet mogen worden (zoals "verberg deze pagina uit alle zoekresultaten") worden niet weggehaald, dus een externe partij kan pagina's onzichtbaar maken. Twee: de bron waaraan het toegangstoken gekoppeld is wordt genegeerd, alles belandt in de standaardbron. Een client die alleen bij "team-b" mag, schrijft dus overal. Het antwoord meldt intussen een bron die nooit gebruikt is.

Waar: src/commands/serve-http.ts regel 1888 tot 1897, en src/core/minions/handlers/ingest-capture.ts regel 116.

Fix: dezelfde vlag en dezelfde bron meegeven als de reguliere schrijfroute dat doet.

16. Bestands-functies van gbrain werken niet op een lokaal brein

Wat gaat mis: drie functies (bestanden opsommen, uploaden, link opvragen) gebruiken de oude vaste databaseverbinding in plaats van de meegegeven engine. Op een brein dat met de aanbevolen lokale variant is aangemaakt, geven ze allemaal een fout die zegt dat de database niet is ingericht. Nagemeten en bevestigd. Een subagent die die melding krijgt, concludeert dat het brein stuk is terwijl het gewoon werkt.

Waar: src/core/operations.ts, regel 2675, 2726 en 2777.

Fix: dezelfde engine-aanpak gebruiken als de commandoregel-versie al doet.

17. Twee database-motoren van 6000 regels die uit de pas lopen

Wat gaat mis: er zijn twee bijna identieke bestanden met dezelfde 143 functies. Elke fix moet twee keer, en niets waarschuwt als dat vergeten wordt. Dat is al gebeurd: één filter (zoeken op paginatype) staat wel in de ene motor en niet in de andere. Op een lokaal brein komen er dan ook andere paginasoorten terug dan gevraagd.

Waar: src/core/pglite-engine.ts regel 228 en 1749, tegenover src/core/postgres-engine.ts.

Fix: de gedeelde SQL in één basisklasse zetten, en een test toevoegen die dezelfde vragen aan beide motoren stelt.

18. Een timeout in gbrain doodt taken die nog maar net gestart zijn

Wat gaat mis: de bewaking rekent de looptijd vanaf de allereerste poging, inclusief alle wachttijd tussen herhalingen. Na een paar mislukte pogingen wordt de nieuwe, net gestarte poging meteen als dood gemarkeerd. De taak krijgt zo nooit zijn volle looptijd. Kanttekening: de standaarddrempel is ruimer dan gemeld, dus de ernst valt in de praktijk mee.

Waar: src/core/minions/queue.ts, regel 742.

Fix: bij elke nieuwe poging de starttijd opnieuw zetten, of een aparte kolom per poging gebruiken.

19. Drie checks van de beveiligingsscanner kunnen nooit iets vinden

Wat gaat mis: dit is het gevaarlijkste soort fout, want alles ziet er groen uit.

Waar: checkers/external/wordpress_checker.py regel 85, checkers/external/bundle_scanner.py regel 54 en 94.

Fix: zoeken op de velden die WordPress echt teruggeeft. Downloadfouten als eigen bevinding melden. En in het rapport zetten: zoveel gevonden, zoveel echt gescand.

20. De scanner slaat vals alarm bij een nette weigering

Wat gaat mis: de scanner test of een chatbot te misleiden is. Hij ziet het als geslaagde hack zodra het antwoord de gewone woorden "instructions" of "system prompt" bevat. Antwoordt de bot netjes met "ik kan mijn instructies niet negeren", dan komt er een kritiek alarm terwijl er niets mis is. Precies de valse melding waar het commentaar in een ander bestand al voor waarschuwt.

Waar: checkers/external/injection_tester.py, regel 21.

Fix: alleen alarm slaan op unieke controlewoorden, en eerst kijken of het antwoord de vraag niet gewoon terugkaatst.


Per repo, de rest

haarvisie-nieuw (nieuwe website met afspraakformulier)

93 bestanden gelezen, 25 bevindingen, allemaal bevestigd. Naast de twee zware hierboven:

gbrain (het gedeelde geheugen)

158 bestanden gelezen, 28 meldingen, 26 bevestigd, 2 afgevallen. Naast de zware hierboven:

davinci-resolve-mcp-free (video-montage)

27 bestanden gelezen, 13 meldingen, 11 bevestigd, 2 afgevallen. Naast de zware hierboven:

haarvisie-security-agent (de beveiligingsscanner)

14 bestanden gelezen, 17 meldingen, 15 bevestigd, 2 afgevallen. Naast de zware hierboven:

fleet-backup-lenovo (de Lenovo-bot)

28 bestanden gelezen, 21 meldingen, 19 bevestigd, 2 afgevallen. Naast de zware hierboven:


Wat er afviel bij de controle

8 van de 104 meldingen zijn afgevallen. Ze klonken plausibel, maar hielden geen stand toen ze werden nagemeten. Dat is precies de bedoeling: liever streng dan een lijst met verzinsels.

Twee voorbeelden:

Andere afvallers: een crash bij het wegschrijven van het rapport (bleek in geen enkele realistische situatie te gebeuren, met lege omgeving nagemeten), een telefoonnummer dat "gelekt" zou zijn maar gewoon op je website staat, een lek dat door een andere regel al werd afgevangen, en een geheugenlek dat door een bestaande opruimfunctie wordt gedekt.

Eén afvaller leverde iets beters op: bij het natrekken van de "telefoonnummer in de repo"-melding kwam de echte vondst boven water, namelijk het GitHub-token in .git/config (punt 2 hierboven).


Tabel per repo

Repo Bestanden gelezen Gemeld Bevestigd
haarvisie-nieuw 93 25 25
gbrain 158 28 26
davinci-resolve-mcp-free 27 13 11
haarvisie-security-agent 14 17 15
fleet-backup-lenovo 28 21 19
Totaal 320 104 96
🎙️ Bespreek met Jarvis