Cómo prevenir SQL injection en Go

Clase 5 de 30Curso de Ciberseguridad para Desarrollo Web

Resumen

Construye un flujo robusto en Go para guardar un webhook de GitHub en base de datos con un repositorio seguro. Aquí verás cómo modelar el payload, definir una entidad para la tabla de commits, crear una interfaz Commit para poder mockear, y usar placeholders con database/sql para prevenir SQL injection con total claridad.

¿Cómo modelar el webhook y reducir el payload en Go?

Al recibir el webhook, la clave está en quedarse solo con lo útil. Se propone crear un paquete models y pedir a una IA (por ejemplo, Google Gemini) una estructura inicial a partir del JSON del payload, para luego recortar campos. La meta: usar solo head commit y datos del repository que importan.

¿Qué campos del webhook son esenciales?

  • repository.full_name: nombre completo del repositorio.
  • head commit: solo un commit, no la lista completa.
  • En el commit: id, message, author.username y author.email.
  • Se descartan: user, pusher y otros campos no usados.

¿Cómo queda el modelo en Go?

package models

type Author struct {
    Username string `json:"username"`
    Email    string `json:"email"`
}

type Commit struct {
    ID      string `json:"id"`
    Message string `json:"message"`
    Author  Author `json:"author"`
}

type Repository struct {
    FullName string `json:"full_name"`
}

type Webhook struct {
    Repository Repository `json:"repository"`
    Commit     Commit     `json:"head_commit"`
}

Con esto, el webhook se puede hacer unmarshal a un modelo claro y mínimo, listo para persistir lo relevante.

¿Qué entidad de base de datos representa cada commit?

Antes del repository, define una estructura de entidad que represente una fila en la tabla commits. Crea un paquete entity y una estructura que incluya metadatos y el payload completo para auditoría.

¿Qué atributos guarda cada fila?

  • ID de la fila.
  • repo_full_name: nombre del repositorio.
  • commit_id y message.
  • author_username y author_email.
  • payload: el JSON del webhook para trazabilidad.
  • created_at y updated_at usando la librería de Go de time.

¿Cómo se define en Go?

package entity

import "time"

type Commit struct {
    ID             int64     `db:"id"`
    RepoFullName   string    `db:"repo_full_name"`
    CommitID       string    `db:"commit_id"`
    Message        string    `db:"message"`
    AuthorUsername string    `db:"author_username"`
    AuthorEmail    string    `db:"author_email"`
    Payload        []byte    `db:"payload"`
    CreatedAt      time.Time `db:"created_at"`
    UpdatedAt      time.Time `db:"updated_at"`
}

Esta entidad centraliza lo que tu engineering manager necesita para analizar commits por desarrollador y mantener historial.

¿Cómo crear un repositorio en Go y prevenir SQL injection?

Crea una carpeta repository y define una interfaz para poder mockear y testear. Implementa métodos clave: Insert y GetByAuthorEmail, siempre con context y placeholders.

¿Cómo luce la interfaz commit?

package repository

import (
    "context"
    "database/sql"

    "your/module/entity"
)

type Commit interface {
    Insert(ctx context.Context, c entity.Commit) error
    GetByAuthorEmail(ctx context.Context, email string) ([]entity.Commit, error)
}

type commitRepo struct { db *sql.DB }

func NewCommit(db *sql.DB) Commit { return &commitRepo{db: db} }

Tener una interfaz permite inyectar dobles en pruebas y desacoplar la base de datos.

¿Qué consulta evita SQL injection?

Nunca concatener strings para armar queries. Ejemplo inseguro:

// Inseguro: expuesto a SQL injection
q := "INSERT INTO commits (author_email) VALUES (" + email + ")"

Usa placeholders y parámetros, para que un ; delete from ... quede como string y no se ejecute:

func (r *commitRepo) Insert(ctx context.Context, c entity.Commit) error {
    const q = `
        INSERT INTO commits
            (repo_full_name, commit_id, message, author_username, author_email, payload, created_at, updated_at)
        VALUES
            (?, ?, ?, ?, ?, ?, ?, ?)`

    _, err := r.db.ExecContext(ctx, q,
        c.RepoFullName,
        c.CommitID,
        c.Message,
        c.AuthorUsername,
        c.AuthorEmail,
        c.Payload,
        c.CreatedAt,
        c.UpdatedAt,
    )
    return err
}

Esto aplica lo aprendido: database/sql, context, y placeholders para proteger tus operaciones.

¿Cómo obtener commits por author email?

func (r *commitRepo) GetByAuthorEmail(ctx context.Context, email string) ([]entity.Commit, error) {
    const q = `
        SELECT id, repo_full_name, commit_id, message, author_username, author_email, payload, created_at, updated_at
        FROM commits
        WHERE author_email = ?`

    rows, err := r.db.QueryContext(ctx, q, email)
    if err != nil { return nil, err }
    defer rows.Close()

    var out []entity.Commit
    for rows.Next() {
        var c entity.Commit
        if err := rows.Scan(
            &c.ID, &c.RepoFullName, &c.CommitID, &c.Message,
            &c.AuthorUsername, &c.AuthorEmail, &c.Payload,
            &c.CreatedAt, &c.UpdatedAt,
        ); err != nil {
            return nil, err
        }
        out = append(out, c)
    }
    return out, rows.Err()
}

Con este método, tu repositorio devuelve un arreglo de commits filtrados por author_email, listo para reportes y análisis.

¿Qué dudas quieres compartir?

¿Te gustaría ver pruebas unitarias con mocking del repositorio o ajustar el modelo del webhook para nuevos campos del payload? Comparte tus preguntas y experiencias en los comentarios: construyamos mejoras entre todos.