· 7 min read

Le danger des secrets dans Git

Vous avez commité des secrets dans votre dépôt Git et vous avez paniqué ? Vous avez bien fait ! Dans ce article nous allons voir comment les secrets peuvent être récupérés et exploités par des pirates ☠️.

Vous avez commité des secrets dans votre dépôt Git et vous avez paniqué ? Vous avez bien fait ! Dans ce article nous allons voir comment les secrets peuvent être récupérés et exploités par des pirates ☠️.

Introduction

Imaginez la situation suivante : vous avez un dépôt Git public sur lequel vous avez commité des par erreurs, des secrets ou des informations sensibles (coucou le fichier .env.local 😱).

Avant que votre commit malencontreux ne soit visible du reste de l’équipe, vous vous refaites un deuxième commit pour supprimer les secrets (adieu le fichier .env.local 😢), et vous vous dites que c’est réglé et que vous pouvez continuer à travailler sans risque.

En êtes-vous bien sûr ? Êtes-vous certains que plus personne ne pourra pas récupérer vos secrets ? 🤔 (Spoiler : Non, c’est encore possible, très simplement !).

L’expérience

Pour illustrer ce problème, nous allons utiliser un dépôt Git public sur lequel nous avons commité des secrets.

En parallèle, nous mettons en ligne une vraie-fausse API qui va nous permettre de logger les exploits de notre dépôt Git.

La vraie-fausse API

  • Créons un nouveau projet Symfony :
symfony new FakeApi --webapp
cd FakeApi
  • Créons un contrôleur :
symfony console make:controller Api
  • Installons le package composer require fakerphp/faker, il nous permettra de générer des données aléatoires

  • Créons une entité pour logger les exploits :

<?php

namespace App\Entity;

use App\Repository\LogRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: LogRepository::class)]
class Log
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $ip = null;

    #[ORM\Column(length: 255)]
    private ?string $userAgent = null;

    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
    private ?\DateTimeInterface $date = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getIp(): ?string
    {
        return $this->ip;
    }

    public function setIp(string $ip): static
    {
        $this->ip = $ip;

        return $this;
    }

    public function getUserAgent(): ?string
    {
        return $this->userAgent;
    }

    public function setUserAgent(string $userAgent): static
    {
        $this->userAgent = $userAgent;

        return $this;
    }

    public function getDate(): ?\DateTimeInterface
    {
        return $this->date;
    }

    public function setDate(\DateTimeInterface $date): static
    {
        $this->date = $date;

        return $this;
    }
}
  • Modifions notre controller pour simuler une vraie-fausse API.

Comme c’est une fausse API, la clé d’API est hardcodée pour rendre la démo la plus simple possible !

<?php

namespace App\Controller;

use App\Entity\Log;
use App\Repository\LogRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Faker;

class ApiController extends AbstractController
{
    const API_KEY = 'APK-6e6eca93-5ce1-471a-bd4a-ceb639878e48';
    const API_HEADER = 'X-Api-Key';

    public function __construct(
        private readonly LogRepository $logRepository,
    ) {
    }

    #[Route('/', name: 'app_api', methods: ['GET'])]
    public function index(Request $request): JsonResponse
    {
        if (!$this->checkApiKey($request)) {
            return $this->json([
                'message' => 'Invalid API Key',
            ], JsonResponse::HTTP_UNAUTHORIZED);
        }

        $this->logRequest($request);

        return $this->json([
            $this->getFakeData(),
        ]);
    }

    private function logRequest(Request $request): void
    {
        $log = new Log();

        $log
            ->setDate(new \DateTime())
            ->setIp($request->getClientIp())
            ->setUserAgent($request->headers->get('User-Agent'))
        ;

        $this->logRepository->save($log);
    }

    private function checkApiKey(Request $request): bool
    {
        $apiKey = $request->headers->get(self::API_HEADER);

        return $apiKey === self::API_KEY;
    }

    private function getFakeData(): array
    {
        $fakeUsers = [];

        for ($i = 0; $i < 10; $i++) {
            $fakeUsers[] = $this->createOneFakeUser();
        }

        return $fakeUsers;
    }

    private function createOneFakeUser(): array
    {
        $faker = Faker\Factory::create();

        return [
            'uuid' => $faker->uuid,
            'username' => $faker->userName,
            'password_hash' => md5($faker->password),
            'first_name' => $faker->firstName,
            'last_name' => $faker->lastName,
            'email' => $faker->email,
            'address' => $faker->address,
            'phone' => $faker->phoneNumber,
            'credit_card_number' => $faker->creditCardNumber,
            'credit_card_expiration' => $faker->creditCardExpirationDate()->format('Y-m-d'),
            'credit_card_cvv' => $faker->randomNumber(3),
            'last_transaction_date' => $faker->dateTimeThisYear('-1 month')->format('Y-m-d'),
            'last_transaction_total' => $faker->randomFloat(2, 0, 100),
            'last_transaction_uuid' => $faker->uuid,
        ];
    }
}
  • Créons une commande pour afficher facilement les dernièrs exploits enregistrés :
<?php

namespace App\Command;

use App\Repository\LogRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:check-logs',
    description: 'List all logs',
)]
class CheckLogsCommand extends Command
{
    public function __construct(
        private readonly LogRepository $logRepository,
    )
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        
        // last 25 logs
        $logs = $this->logRepository->findBy([], ['id' => 'DESC'], 25);

        $io->table(
            ['date', 'id', 'ip', 'user_agent'],
            array_map(fn($log) => [
                $log->getDate()->format('Y-m-d H:i:s'),
                $log->getId(),
                $log->getIp(),
                $log->getUserAgent(),
            ], $logs)
        );

        return Command::SUCCESS;
    }
}
  • Et mettons en production notre application Symfony : Je ne détaillerai pas les différentes étapes ici, d’autres articles du bloget d’autres vidéos YouTube abordent dèjà cette partie.

Le repository “leak”

Maintenant que notre vraie-fausse API est en production, nous allons faire fuiter son url, la clé d’API et les secrets de un autre repository Git public sur GitHub.

Si vous consultez l’historique des commits, vous verrez que le repository contient des nombreux commits faisant référence à des secrets ajoutés, puis enlevés, et enfin supprimés… pour attirer l’attention de bots de détection de secrets.

J’ai ensuite attendu quelques jours pour voir si des secrets ont été récupérés… hélas, pas de secrets récupérés, peut être que les bots ont senti la supercherie 😉.

J’ai donc solicité la communauté sur Twitter, et quelques minutes plus tard, la clé d’API a été récupérée et utilisée sur notre vraie-fausse API 👀.

Comment vérifier si des secrets sont visibles dans un dépôt Git (ou son historique) ?

Maintenant la bonne question à ce poser est : comment vérifier si des secrets sont visibles dans un dépôt Git (ou son historique) ?

Je ne serez que vous conseiller l’outil GitLeaks, qui permet de scanner un dépôt Git et de détecter les secrets dans le code source.

Pour tester, n’hesitez pas à cloner le dépôt Git public BlockchainChecker et de scanner son historique avec GitLeaks.

git@github.com:yoanbernabeu/BlockchainChecker.git
cd BlockchainChecker

# Scanner le historique
gitleaks git .
gitleaks git


    │╲


    gitleaks

2:46PM INF 10 commits scanned.
2:46PM INF scan completed in 80.6ms
2:46PM WRN leaks found: 32

On le voit, GitLeaks a trouvé 32 secrets dans le historique du dépôt Git public 😱 !

Comment supprimer définitivement les secrets dans un dépôt Git ?

Il existe de nombreux outils pour supprimer des secrets dans un dépôt Git, certains “de base” comme git filter-branch, ou d’autres “plus avancés” comme git-filter-repo.

Tous ces outils permettent de supprimer des secrets dans un dépôt Git, mais aucuns ne permettent de le faire de manière hyper simple !

Pour l’occasion de cet article/vidéo, je me suis dev une petite CLI en GO, qui permet de supprimer des secrets dans un dépôt Git, et de le faire de manière hyper simple en une seule commande !

L’outil s’appelle Git Cleaner, et son objectif est de supprimer UN fichier dans TOUS les commits d’un dépôt Git en une passe ! (Attention, il n’est probablement pas encore totalement stable).

Dans notre exemple, nous allons supprimer les fichiers .env.local, .env.local.php et .env de notre dépôt Git public BlockchainChecker.

git-cleaner --file .env.local
git-cleaner --file .env.local.php
git-cleaner --file .env

Et nous pouvons vérifier avec GitLeaks si les secrets sont toujours visibles :

gitleaks git .


    │╲


    gitleaks

2:55PM INF 7 commits scanned.
2:55PM INF scan completed in 79.9ms
2:55PM INF no leaks found

GitLeaks nous confirme que nous avons bien néttoyé les secrets de notre dépôt et de son historique !

Si cela ce produit, voici quelques bonnes pratiques à suivre :

Ce qui suit est issue de mes expériences personnelles, et donc peut être très subjectif. Chaque entreprise a ses propres besoins et ses propres règles, il est donc important de suivre les bonnes pratiques de votre entreprise (si elles existent 👀).

Dans l’instant présent et le plus rapidement possible, vous devez :

  1. Prevenir immédiatement votre équipe, votre Lead Dev, cotre CTO, votre RSSI ou tous autres personnes de votre organisation : NE RESTEZ PAS SEULE FACE À CETTE SITUATION !
  2. Inactivées TOUTES les clés API et autres secrets ayant été commités dans votre dépôt Git, même si cela peut rendre une partie de vos applications indisponibles : LA SECURITE AVANT TOUT !
  3. Supprimez et nettoyez tous les secrets de votre dépôt Git et COMMITTEZ VOS CHANGEMENTS IMMEDIATEMENT !

Une fois la situation sous contrôle, potentiellement quelques heures/jours après, vous devriez :

  1. Evaluer si des données sensibles, de vous ou vos clients ont pu êtres exposées, voir pire exploitées
  2. Si oui, prevenir vos utilisateurs et vos clients avec la maximum de transparence possible

Ultérieurement, vous devriez :

  1. Ecrire un post-mortem pour expliquer ce qui a mené à cette situation et comment vous avez résolu le problème
  2. Mettre en place des solutions de sécurité pour éviter de reproduire ce problème dans le futur
  3. Former vos équipes à la sécurité !

Conclusion

Dans ce tutoriel, nous avons vu comment les secrets peuvent être récupérés et exploités par des pirates ☠️.

Un simple commit dans un dépôt Git peut être exploité et pourrait avoir des conséquences graves sur votre projet, et sur votre entreprise.

Back to Blog