<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Lucas Pelegrina</title>
    <description>The latest articles on Forem by Lucas Pelegrina (@lucas_pelegrina_16524837c).</description>
    <link>https://forem.com/lucas_pelegrina_16524837c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3688133%2Ffde7f0f0-41c6-4135-b3b8-98b6ccb5d326.jpg</url>
      <title>Forem: Lucas Pelegrina</title>
      <link>https://forem.com/lucas_pelegrina_16524837c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lucas_pelegrina_16524837c"/>
    <language>en</language>
    <item>
      <title>Quando concorrência vira vulnerabilidade: explorando uma race condition em Go</title>
      <dc:creator>Lucas Pelegrina</dc:creator>
      <pubDate>Fri, 02 Jan 2026 03:00:00 +0000</pubDate>
      <link>https://forem.com/lucas_pelegrina_16524837c/quando-concorrencia-vira-vulnerabilidade-explorando-uma-race-condition-em-go-143p</link>
      <guid>https://forem.com/lucas_pelegrina_16524837c/quando-concorrencia-vira-vulnerabilidade-explorando-uma-race-condition-em-go-143p</guid>
      <description>&lt;h2&gt;
  
  
  Introdução
&lt;/h2&gt;

&lt;p&gt;Hoje vamos falar sobre race conditions em Golang e como um detalhe aparentemente inocente no código pode se transformar em uma vulnerabilidade real em uma API web.&lt;/p&gt;

&lt;p&gt;Para fugir da teoria pura, este post utiliza como exemplo prático um desafio do HackingClub chamado Toc-toc, nome que faz referência direta à vulnerabilidade TOCTOU (Time-of-Check to Time-of-Use).&lt;/p&gt;

&lt;p&gt;A aplicação analisada consiste em um servidor escrito em Go, com o código-fonte exposto publicamente por uma rota acessível, cujo objetivo é encontrar uma forma de ler a flag armazenada no servidor. A partir dessa aplicação, foi possível identificar uma condição de corrida em um endpoint de auditoria de logs e explorá-la para escapar do diretório esperado e acessar arquivos sensíveis do sistema.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiaxj6w3ddxmpbflnq7v8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiaxj6w3ddxmpbflnq7v8.png" alt="Imagem do desafio Toc-toc" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Primeiro Contato
&lt;/h2&gt;

&lt;p&gt;Ao acessar o IP da aplicação, percebemos que o código-fonte estava disponível diretamente na rota raiz. Isso já nos dá uma excelente superfície de análise. Vamos, então, examiná-lo com mais atenção.&lt;/p&gt;

&lt;p&gt;A seguir está o código completo da aplicação:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "path/filepath"
    "strings"

    "github.com/gorilla/mux"
)

type AuditResponse struct {
    Log     string `json:"log"`
    Content string `json:"content"`
}

type ErrorResponse struct {
    Message string `json:"message"`
}

func sanitizeFileName(name string) string {
    return filepath.Base(name)
}

func checkPathTraversal(name string) bool {
    if strings.Contains(name, "..") || strings.Contains(name, "/") {
        return true
    }

    return false
}

func main() {
    err := os.MkdirAll("logs", 0755)
    if err != nil {
        fmt.Println("Error to create logs directory:", err)
        return
    }

    filePath := ""

    r := mux.NewRouter()

    r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/plain")

        content, err := os.ReadFile("main.go")
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte("Error reading source code"))
            return
        }

        w.Write(content)
    }).Methods("GET")

    r.HandleFunc("/audit", func(w http.ResponseWriter, r *http.Request) {
        logParam := r.URL.Query().Get("log")

        if logParam != "" {
            filePath = logParam

            pathTraversal := checkPathTraversal(filePath)
            if pathTraversal {
                filePath = sanitizeFileName(filePath)
            }
        } else {
            filePath = "last-activity.txt"
        }

        filePath = filepath.Join("logs", filePath)

        content, err := os.ReadFile(filePath)
        if err != nil {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusNotFound)

            errorResponse := ErrorResponse{
                Message: "Log not found",
            }

            json.NewEncoder(w).Encode(errorResponse)
            return
        }

        response := AuditResponse{
            Log:     logParam,
            Content: string(content),
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(response)
    }).Methods("GET")

    r.HandleFunc("/audit/list", func(w http.ResponseWriter, r *http.Request) {
        files, err := os.ReadDir("logs")

        if err != nil {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusInternalServerError)

            errorResponse := ErrorResponse{
                Message: "Could not list logs",
            }

            json.NewEncoder(w).Encode(errorResponse)
            return
        }

        var logFiles []string
        for _, file := range files {
            if !file.IsDir() {
                logFiles = append(logFiles, file.Name())
            }
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(logFiles)
    }).Methods("GET")

    // r.HandleFunc("/get-flag", func(w http.ResponseWriter, r *http.Request) {
    //  w.Header().Set("Content-Type", "text/plain")

    //  content, err := os.ReadFile("./flag.txt")
    //  if err != nil {
    //      w.WriteHeader(http.StatusInternalServerError)
    //      w.Write([]byte("Error reading flag"))
    //      return
    //  }

    //  w.Write(content)

    // }).Methods("GET")

    fmt.Println("Server running on port 8000...")
    http.ListenAndServe(":8000", r)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Analisando o Código
&lt;/h2&gt;

&lt;p&gt;Durante a análise do código, um detalhe aparentemente inofensivo chama a atenção logo no início da função &lt;code&gt;main&lt;/code&gt;. É o tipo de coisa que facilmente passa em um code review apressado e exatamente por isso merece cuidado. A variável: &lt;code&gt;filePath := ""&lt;/code&gt;, que está declarada no escopo do &lt;code&gt;main()&lt;/code&gt;, fora de qualquer handler HTTP. Isso faz com que ela se torne uma variável global compartilhada entre todas as requisições processadas pelo servidor.&lt;/p&gt;

&lt;p&gt;No pacote &lt;strong&gt;net/http&lt;/strong&gt;, cada requisição recebida é tratada em sua &lt;strong&gt;própria goroutine&lt;/strong&gt;. Isso significa que &lt;strong&gt;múltiplas requisições podem acessar e modificar essa variável simultaneamente&lt;/strong&gt;. Como não há nenhum mecanismo de sincronização (como mutex), cria-se um cenário clássico de race condition.&lt;/p&gt;

&lt;p&gt;Essa variável compartilhada é, de fato, o coração da vulnerabilidade.&lt;br&gt;
​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func main() {
    err := os.MkdirAll("logs", 0755)
    if err != nil {
        fmt.Println("Error to create logs directory:", err)
        return
    }

    ---&amp;gt;filePath := ""&amp;lt;-------------AQUI!----------------------

    r := mux.NewRouter()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mas por que isso é tão crítico?
&lt;/h3&gt;




&lt;h2&gt;
  
  
  Entendendo a Vulnerabilidade
&lt;/h2&gt;

&lt;p&gt;Antes de falar da exploração em si, vale dar um passo atrás e entender por que esse tipo de bug existe — e por que ele é tão perigoso em sistemas concorrentes.&lt;/p&gt;

&lt;p&gt;A vulnerabilidade explorada aqui pertence à classe &lt;strong&gt;TOCTOU&lt;/strong&gt; (Time-of-Check to Time-of-Use), uma forma clássica de race condition. Ela ocorre quando uma verificação de segurança é feita em um determinado momento (&lt;strong&gt;check&lt;/strong&gt;), mas o recurso verificado é utilizado apenas posteriormente (&lt;strong&gt;use&lt;/strong&gt;). Entre esses dois pontos existe uma &lt;strong&gt;janela crítica&lt;/strong&gt;, na qual o estado do sistema &lt;strong&gt;pode mudar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No contexto dessa aplicação, o problema fica evidente porque:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O valor de &lt;code&gt;filePath&lt;/code&gt; é validado em um momento&lt;/li&gt;
&lt;li&gt;Esse mesmo valor é utilizado posteriormente para leitura de arquivo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entre esses dois momentos&lt;/strong&gt;, outra requisição &lt;strong&gt;pode alterar completamente o conteúdo da variável&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Como cada handler roda em sua própria goroutine, &lt;strong&gt;múltiplas requisições competem simultaneamente por essa variável compartilhada&lt;/strong&gt;, criando o cenário perfeito para exploração.&lt;/p&gt;




&lt;h2&gt;
  
  
  Indo para a Exploração
&lt;/h2&gt;

&lt;p&gt;Vamos analisar o fluxo crítico do endpoint /audit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A aplicação lê o parâmetro &lt;code&gt;log&lt;/code&gt; da query string&lt;/li&gt;
&lt;li&gt;Esse valor é atribuído à variável global &lt;code&gt;filePath&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A função &lt;code&gt;checkPathTraversal&lt;/code&gt; verifica se o valor contém &lt;code&gt;..&lt;/code&gt; ou &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Caso algo suspeito seja detectado, o valor é sanitizado com &lt;code&gt;sanitizeFileName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Em seguida, o código executa: &lt;code&gt;filePath = filepath.Join("logs", filePath)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Por fim, chama &lt;code&gt;os.ReadFile(filePath)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;O problema central é que a &lt;strong&gt;validação e o uso do valor não são atômicos.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a seguinte sequência:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requisição A&lt;/strong&gt; chega com &lt;code&gt;?log=safe.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filePath = "safe.txt"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Passa na verificação&lt;/li&gt;
&lt;li&gt;Antes de &lt;code&gt;os.ReadFile&lt;/code&gt; ser executado, chega a &lt;strong&gt;Requisição B&lt;/strong&gt; com &lt;code&gt;?log=../flag.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filePath&lt;/code&gt; é sobrescrito para &lt;code&gt;"../flag.txt"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Requisição A&lt;/strong&gt; continua sua execução&lt;/li&gt;
&lt;li&gt;Executa &lt;code&gt;filepath.Join("logs", "../flag.txt")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;O caminho é resolvido para fora do diretório logs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os.ReadFile&lt;/code&gt; lê o arquivo sensível da flag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mesmo que &lt;code&gt;filepath.Join&lt;/code&gt; normalize o caminho, ele não oferece nenhuma proteção &lt;strong&gt;contra condições de corrida&lt;/strong&gt; quando a variável de entrada &lt;strong&gt;pode ser alterada entre a validação e o uso&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exploração Prática
&lt;/h2&gt;

&lt;p&gt;A exploração foi realizada utilizando o Caido, explorando requisições concorrentes.&lt;/p&gt;

&lt;p&gt;O endpoint &lt;code&gt;/audit/list&lt;/code&gt; permitiu identificar nomes de arquivos válidos dentro do diretório logs, o que ajudou a construir payloads que passavam facilmente pela verificação inicial.&lt;/p&gt;

&lt;p&gt;Ao disparar múltiplas requisições simultâneas, algumas com parâmetros aparentemente legítimos e outras com caminhos maliciosos. Após algumas tentativas, a resposta retornou o conteúdo da flag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy67z2az5bxacxph9emm1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy67z2az5bxacxph9emm1.png" alt="Imagem do caido com requisição e payload utilizado" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Entendendo a Criticidade
&lt;/h2&gt;

&lt;p&gt;Essa vulnerabilidade é particularmente crítica porque:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Não exige autenticação&lt;/li&gt;
&lt;li&gt;Pode ser explorada remotamente&lt;/li&gt;
&lt;li&gt;Permite leitura arbitrária de arquivos&lt;/li&gt;
&lt;li&gt;Pode levar ao vazamento de credenciais, chaves, tokens ou configurações sensíveis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Além disso, falhas de race condition são notoriamente difíceis de detectar em code reviews e quase sempre passam despercebidas por scanners automáticos, já que dependem de comportamento concorrente e timing preciso.&lt;/p&gt;




&lt;h2&gt;
  
  
  Como Perceber e Corrigir
&lt;/h2&gt;

&lt;p&gt;Mais do que corrigir esse bug específico, o ponto aqui é entender o padrão de falha. Vulnerabilidades como essa não surgem do nada. Elas são resultado de decisões de design que não consideraram concorrência como uma superfície de ataque.&lt;/p&gt;

&lt;p&gt;Mas bem, a primeira lição aqui é simples: handlers HTTP nunca devem compartilhar estado mutável sem sincronização.&lt;/p&gt;

&lt;p&gt;A correção mais adequada neste caso é eliminar completamente o estado global. A variável filePath deve ser local ao handler, garantindo que cada requisição trabalhe com seu próprio contexto.&lt;/p&gt;

&lt;p&gt;Uma correção simples e eficaz seria: &lt;code&gt;filePath := filepath.Join("logs", sanitizeFileName(logParam))&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Além disso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Evite validações separadas do uso do recurso&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefira operações atômicas&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Mutexes podem mitigar o problema, mas não são a melhor solução nesse cenário&lt;/li&gt;
&lt;li&gt;O ideal é &lt;strong&gt;não compartilhar estado entre requisições&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Além disso, há uma ferramenta extremamente útil para identificar esse tipo de problema em aplicações Go: o &lt;strong&gt;Data Race Detector&lt;/strong&gt; nativo da linguagem.&lt;/p&gt;

&lt;p&gt;O Go fornece suporte integrado para detecção de &lt;em&gt;data races&lt;/em&gt; em tempo de execução, permitindo identificar acessos concorrentes não sincronizados à memória compartilhada. Essa ferramenta é especialmente eficaz para encontrar falhas que dificilmente seriam percebidas apenas com testes funcionais ou code review.&lt;/p&gt;

&lt;p&gt;Durante o desenvolvimento e testes, é altamente recomendado executar a aplicação ou os testes automatizados com a flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go run -race main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;

&lt;p&gt;Este desafio mostra como uma única variável mal posicionada pode comprometer completamente a segurança de uma aplicação concorrente. Em linguagens como Go, onde concorrência é simples e eficiente, erros desse tipo são fáceis de introduzir e perigosos de ignorar.&lt;/p&gt;

&lt;p&gt;Race conditions e vulnerabilidades TOCTOU não são apenas problemas teóricos. Elas existem, são exploráveis e podem ter impactos severos em ambientes reais.&lt;/p&gt;




&lt;h2&gt;
  
  
  Referências
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cwe.mitre.org/data/definitions/367.html" rel="noopener noreferrer"&gt;MITRE. CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition.  &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://portswigger.net/web-security/race-conditions" rel="noopener noreferrer"&gt;Portswigger. Race Condition.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pkg.go.dev/net/http" rel="noopener noreferrer"&gt;Go Documentation. net/http Package.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://go.dev/blog/waza-talk" rel="noopener noreferrer"&gt;Go Blog. Concurrency is not Parallelism.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://owasp.org/www-community/attacks/Path_Traversal" rel="noopener noreferrer"&gt;OWASP. Path Traversal.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pkg.go.dev/path/filepath" rel="noopener noreferrer"&gt;Go Documentation. filepath Package.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://go.dev/doc/articles/race_detector" rel="noopener noreferrer"&gt;Go Wiki. Race Detector.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://go.dev/ref/mem" rel="noopener noreferrer"&gt;The Go Programming Language. The Go Memory Model.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>webdev</category>
      <category>go</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
