In this article
Injeção de SQL em Golang por exemplo
Se você está começando no desenvolvimento de back-end com Golang, é importante conhecer as convenções de programação segura. Um dos aspectos da segurança de aplicações é entender o que significa injeção de SQL em Go e como corrigir vulnerabilidades desse tipo no código.
Vamos usar o framework de aplicações web Gin para Golang, uma biblioteca SQLite e rotas de servidor HTTP para interagir com interfaces SQL em Golang. Com este experimento, vamos aprender a executar exploits que demonstram a injeção de SQL em um banco de dados SQLite e em outros bancos de dados.
Um programa SQL em Golang
Antes de começar um exemplo de injeção de SQL em Golang, é fundamental configurar um ambiente de desenvolvimento adequado. Isso envolve instalar os módulos necessários do Go e configurar o projeto para gerenciar dependências com eficiência.
Vamos usar os pacotes github.com/mattn/go-sqlite3 e github.com/jmoiron/sqlx para a interface SQL.
Gerenciando dependências com módulos Go
Os módulos Go se tornaram a maneira padrão de gerenciar dependências em projetos Go. Eles permitem especificar as versões dos pacotes dos quais seu projeto depende, garantindo consistência entre diferentes ambientes. Para usar módulos Go, você precisa inicializar o projeto com um arquivo go.mod.
Observação: o Snyk oferece ótimo suporte para Golang por meio do Snyk Code, a ferramenta SAST, e do Snyk Open Source, a ferramenta SCA para dependências de terceiros. É sempre recomendável verificar seu código Go em busca de vulnerabilidades — e o Snyk se integra perfeitamente ao seu fluxo de trabalho!
Para começar nosso exemplo de SQL em Golang, acesse o diretório do projeto e execute o seguinte comando:
go mod init your_project_nameEsse comando cria um arquivo go.mod no diretório do projeto, que registra as dependências e suas versões.
Instale o pacote de interface SQL para Golang
O pacote github.com/mattn/go-sqlite3 é uma biblioteca Go que oferece uma interface para bancos de dados SQLite. Ele é essencial para executar consultas SQL em bancos de dados SQLite nos seus programas SQL em Golang e será usado no nosso exemplo de aplicação Go. Para instalar esse pacote, use o seguinte comando:
go get github.com/mattn/go-sqlite3
Esse comando baixa o pacote e o adiciona ao arquivo go.mod. O pacote ficará disponível no seu projeto, permitindo que você interaja com bancos de dados SQLite.
Da mesma forma, também vamos instalar o pacote github.com/jmoiron/sqlx, uma extensão do pacote padrão database/sql do Go. Ele oferece funcionalidades adicionais, como suporte a consultas nomeadas e leitura de structs, que podem simplificar as interações com bancos de dados em aplicações SQL em Golang. Para instalar esse pacote, execute:
go get github.com/jmoiron/sqlxEm seguida, vamos precisar dos pacotes Go no programa, da seguinte forma:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"time"
"log/slog"
// Gin web framework
"github.com/gin-gonic/gin"
// SQLite database driver and query builder
_ "github.com/mattn/go-sqlite3"
"github.com/jmoiron/sqlx"
)Como você pode ver nas instruções de importação dos módulos Go acima, a aplicação Golang será uma API HTTP que disponibiliza rotas GET e POST para desenvolver a demonstração de um programa SQL em Golang.
Interfaces SQL em Golang
Na nossa aplicação web em Golang, o programa se conecta a um banco de dados SQL por meio de uma rota HTTP. A rota HTTP em /cloudpawnery/image atende a solicitações GET e recebe o parâmetro de consulta tenantID. Com ele, busca no banco de dados SQLite todos os registros de arquivos para os quais foram realizadas conversões de imagem.
A interface SQL em Golang é demonstrada pela seguinte API de rota HTTP GET, que retorna uma resposta JSON com arquivos:
router.GET("/cloudpawnery/image", func(c *gin.Context) {
// get tenantID from query parameter
tenantID := c.Query("tenantID")
// pseudo-code :
for loop-over-db-rows
if err != nil {
// handle errors
}
// populate an array of files:
files = append(files, f)
}
// return files as an object in the JSON response
c.JSON(http.StatusOK, gin.H{"files": files})
})Agora vamos nos concentrar na parte da interface SQL em Golang do programa.
Vamos usar o módulo Go sqlx para abrir o banco de dados SQLite e, em seguida, usar a função Queryx para enviar ao banco de dados uma consulta SELECT com um filtro WHERE que usa o tenantID do parâmetro de consulta da solicitação HTTP GET.
db, err := sqlx.Open("sqlite3", "./mydb.db")
defer db.Close()
if err != nil {
slog.Error("Failed to open database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open database"})
return
}
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")
if err != nil {
slog.Error("Failed to query database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query database"})
return
}
defer rows.Close()Agora que temos uma referência aos registros do banco de dados por meio da variável rows, podemos percorrê-la e atribuir os resultados da consulta SQL ao array vazio do tipo File, files.
var files []File
for rows.Next() {
var f File
err := rows.StructScan(&f)
if err != nil {
slog.Error("Failed to scan database row", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database row"})
return
}
files = append(files, f)
}Enquanto houver resultados da consulta SQL para retornar, entramos no loop (daí a chamada rows.Next() ). Em seguida, preparamos uma única entrada File com var f File e usamos a consulta rows.StructScan() do módulo Go sqlx para extrair as informações da consulta e atribuí-las à variável f, do tipo File.
Confira abaixo a API completa da nossa rota HTTP GET:
router.GET("/cloudpawnery/image", func(c *gin.Context) {
// get tenantID from query parameter
tenantID := c.Query("tenantID")
db, err := sqlx.Open("sqlite3", "./mydb.db")
defer db.Close()
if err != nil {
slog.Error("Failed to open database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open database"})
return
}
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")
if err != nil {
slog.Error("Failed to query database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query database"})
return
}
defer rows.Close()
var files []File
for rows.Next() {
var f File
err := rows.StructScan(&f)
if err != nil {
slog.Error("Failed to scan database row", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database row"})
return
}
files = append(files, f)
}
c.JSON(http.StatusOK, gin.H{"files": files})
})Mas… e a segurança da aplicação? O que pode dar errado com o código acima? Bem, a injeção de SQL. Vamos entender melhor!
Injeção de SQL no SQLite
A injeção de SQL é uma vulnerabilidade crítica de segurança que permite que invasores interfiram nas consultas SQL que uma aplicação Golang faz ao banco de dados. Mesmo em 2024, a injeção de SQL continua sendo uma grande preocupação e aparece com frequência em listas de riscos e ataques cibernéticos. Ao manipular dados de entrada, invasores podem executar código SQL arbitrário, possivelmente obtendo acesso não autorizado a dados confidenciais, alterando o conteúdo do banco de dados ou até executando operações administrativas. As consequências podem ser graves, incluindo violações e perda de dados, além de danos significativos à reputação das organizações.
Demonstrando uma vulnerabilidade de injeção de SQL em uma aplicação Golang
Para ilustrar uma vulnerabilidade de injeção de SQL em uma aplicação Golang, vamos ampliar a aplicação acima, que usa o pacote sqlx como interface SQL em Golang.
Sabemos que a interface da API HTTP recebe uma solicitação GET e retorna JSON. Ela recebe um identificador de tenant pelo parâmetro de consulta tenantID. Uma solicitação de um consumidor (ou invasor) poderia ser assim:
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712
Content-Type: application/jsonSe acertarmos o tenantID (e tivermos autorização para acessar esses dados), receberemos uma resposta como esta:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 01 Dec 2024 12:37:05 GMT
Content-Length: 248
Connection: close
{
"files": [
{
"ID": 1,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "3971533981712",
"CreatedAt": "0001-01-01T00:00:00Z"
}
]
}Parece ótimo.
No entanto, poderíamos tentar fazer engenharia reversa da consulta SQL executada pelo programa Golang para fornecer esses dados. Por exemplo, provavelmente ele consulta uma tabela files e filtra os resultados com base em um campo de identificador de tenant, como tenantID, tenant_ID ou tenant_identifier.
Na verdade, se examinarmos o código da consulta que escrevemos para o parâmetro da rota GET, veremos que ele é muito parecido:
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")Esse é justamente um código vulnerável a injeção de SQL que criamos no nosso programa Golang.
O problema está no código que concatena strings para adicionar à própria consulta SQL a variável tenantID, fornecida pelo usuário como parâmetro de consulta. Ao seguir essa convenção insegura de código SQL, criamos uma vulnerabilidade de injeção de SQL no SQLite.
Com isso em mente, fica claro como seria fácil manipular o parâmetro de consulta tenantID para alterar o significado da consulta SQL original no programa Golang. Podemos enviar a seguinte solicitação HTTP:
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712' OR tenant_id='432423
Content-Type: application/jsonO valor de tenantID agora usa uma aspa simples para “fechar” a atribuição do campo da cláusula WHERE e criar uma nova expressão lógica com OR tenant_id=’<some-number>, que filtra outro registro no banco de dados sem incluir uma aspa simples de fechamento. Por que não é preciso incluir a aspa simples de fechamento? Porque a própria consulta vai adicioná-la como parte do código SQL.
Com isso em mente, ao enviar a solicitação HTTP acima, serão retornados registros dos dois tenants:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 01 Dec 2024 12:37:05 GMT
Content-Length: 248
Connection: close
{
"files": [
{
"ID": 1,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "3971533981712",
"CreatedAt": "0001-01-01T00:00:00Z"
},
{
"ID": 2,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "432423",
"CreatedAt": "0001-01-01T00:00:00Z"
}
]
}Injeção de SQL em Golang para autenticação
Para ver um exemplo mais clássico de cenário de injeção de SQL em Golang, que demonstra o que essa vulnerabilidade significa para desenvolvedores, confira o seguinte código Go, que:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./example.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
username := "admin' OR '1'='1"
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, err := db.Query(query)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("User: %d, %s\n", id, name)
}
}O trecho de código Golang acima usa a conhecida técnica de bypass de autenticação por injeção de SQL chamada OR 1=1. É claro que o código também tem muitas outras vulnerabilidades de segurança. Recomendo que você aprenda mais sobre elas, caso não as tenha identificado ao ler o código:
Uso de senhas em texto simples e hash inseguro.
As consequências de vulnerabilidades de injeção de SQL em aplicações podem ser graves. Invasores podem:
Acessar dados confidenciais: obter acesso não autorizado a dados de usuários, como vimos na injeção do identificador de tenant, e a outros registros, como informações financeiras.
Alterar ou excluir dados: embora tenhamos mostrado apenas como selecionar mais dados, alterar ou remover dados críticos também é um risco significativo de injeção de SQL e pode causar problemas de integridade dos dados.
Comprometer a segurança da aplicação: usar a injeção de SQL como ponto de partida para outros tipos de ataque, pois invasores costumam combinar diferentes vulnerabilidades para aumentar o impacto.
Como corrigir a injeção de SQL
Agora que estabelecemos que a injeção de SQL é uma vulnerabilidade de segurança comum e que pode ter consequências graves quando não é corrigida, vamos aprender mais sobre como evitá-la.
Use prepared statements e consultas parametrizadas
Uma das formas mais eficazes de prevenir a injeção de SQL é usar prepared statements e consultas parametrizadas na construção das consultas SQL. Essas técnicas, comuns em muitas bibliotecas, linguagens de programação e plataformas, garantem que a entrada do usuário seja tratada como dado, e não como código executável, impedindo a execução de código SQL malicioso.
Em Golang, podemos continuar usando o pacote sqlx para facilitar o uso de prepared statements (que não ativamos antes na aplicação vulnerável acima).
Veja um exemplo de como implementar consultas SQL seguras usando sqlx:
package main
import (
"fmt"
"log"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
db, err := sqlx.Connect("sqlite3", "example.db")
if err != nil {
log.Fatalln(err)
}
var user User
username := "john_doe"
query := "SELECT id, name FROM users WHERE name = ?"
err = db.Get(&user, query, username)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}Neste código Golang, o caractere ? é usado como espaço reservado para inserir com segurança a variável username na consulta SQL. Isso informa ao mecanismo do banco de dados que o valor é um dado a ser processado, e não parte da consulta SQL. Assim, a entrada é devidamente escapada, prevenindo ataques de injeção de SQL em Golang.
Use o Snyk Code para detectar e corrigir vulnerabilidades de injeção de SQL
Seguir as práticas recomendadas, como usar prepared statements para SQL, é essencial. Mas ferramentas automatizadas podem aumentar ainda mais a segurança do seu código, detectando vulnerabilidades que talvez passem despercebidas. O Snyk Code é uma ferramenta poderosa que ajuda a identificar e corrigir vulnerabilidades de injeção de SQL nas suas aplicações Golang.
Integrar o Snyk ao seu fluxo de trabalho de desenvolvimento oferece vários benefícios:
Análise automatizada: o Snyk Code verifica automaticamente seu código-fonte em busca de vulnerabilidades, incluindo injeção de SQL, e apresenta informações práticas para ajudar você a agir.
Monitoramento contínuo: o Snyk monitora continuamente sua base de código em busca de novas vulnerabilidades, ajudando a manter suas aplicações seguras ao longo do tempo.
Feito para desenvolvedores: o Snyk se integra perfeitamente a IDEs como o VS Code e a outros fluxos de trabalho de desenvolvimento. Assim, desenvolvedores podem incorporar segurança ao trabalho sem prejudicar a produtividade.
Para aproveitar os poderosos recursos de segurança do Snyk e proteger suas aplicações contra vulnerabilidades de injeção de SQL, crie hoje mesmo uma conta gratuita no Snyk. Com o Snyk, você pode garantir que suas aplicações Golang sejam seguras e estejam em conformidade com os padrões do setor.
Recomendo os seguintes recursos para você continuar aprendendo e se aprofundar nas vulnerabilidades de injeção de SQL:
A cheat sheet sobre injeção de SQL de Brian Vermeer, que apresenta 8 práticas recomendadas para prevenir ataques de injeção de SQL.
A cheat sheet de segurança do Go de Eric Smalling.
Por fim, entenda as vulnerabilidades de injeção de comandos em Go
Conheça o cenário atual da segurança de código aberto
Entenda as tendências e abordagens atuais para proteger softwares de código aberto e a cadeia de suprimentos.