Validación de Entradas y Manejo Seguro de HTML/JSON
Cada manejador HTTP y gRPC que acepta entradas de usuario debe validar la estructura, el tamaño y la semántica antes de tocar bases de datos, shells o salidas HTML.
Busca en todas las páginas de la documentación
Cada manejador HTTP y gRPC que acepta entradas de usuario debe validar la estructura, el tamaño y la semántica antes de tocar bases de datos, shells o salidas HTML.
Los analizadores de Go ayudan con la sintaxis, pero no imponen tu política de seguridad.
La validación de entradas en Go comienza en el borde del transporte (límites de tamaño del cuerpo, tipos de contenido) y continúa a través de la decodificación tipada (encoding/json, json.Decoder, protobuf) y verificaciones explícitas de reglas (longitud, formato, listas de permitidos).
La codificación de salida segura depende del contexto: HTML necesita html/template, las API JSON necesitan un Content-Type correcto y ningún HTML incrustado en campos de cadena sin codificar.
Las vulnerabilidades de inyección (SQL, comandos, plantillas, logs) comparten una causa raíz: tratar datos no confiables como código o estructura.
Valida temprano, parametriza consultas y falla con mensajes de error seguros.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
type CreateUser struct {
Email string `json:"email"`
Age int `json:"age"`
}
func decodeCreateUser(r *http.Request) (CreateUser, error) {
r.Body = http.MaxBytesReader(nil, r.Body, 1<<20)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var u CreateUser
if err := dec.Decode(&u); err != nil {
return u, err
}
if !strings.Contains(u.Email, "@") || u.Age < 0 || u.Age > 150 {
return u, errors.New("invalid user")
}
return u, nil
}Cuándo usar esto:
package main
import (
"database/sql"
"encoding/json"
"errors"
"html/template"
"net/http"
"regexp"
"strings"
_ "modernc.org/sqlite"
)
var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
type Profile struct {
DisplayName string `json:"display_name"`
Email string `json:"email"`
}
func validateProfile(p Profile) error {
p.DisplayName = strings.TrimSpace(p.DisplayName)
if len(p.DisplayName) == 0 || len(p.DisplayName) > 64 {
return errors.New("invalid display name")
}
if !emailRe.MatchString(p.Email) {
return errors.New("invalid email")
}
return nil
}
func createProfileHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var p Profile
if err := dec.Decode(&p); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := validateProfile(p); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
_, err := db.Exec(
"INSERT INTO profiles (display_name, email) VALUES (?, ?)",
p.DisplayName, p.Email,
)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
}
func renderProfileHandler() http.HandlerFunc {
tmpl := template.Must(template.New("page").Parse(`<h1>{{.}}</h1>`))
return func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if len(name) > 64 {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, name)
}
}
func main() {
db, _ := sql.Open("sqlite", ":memory:")
mux := http.NewServeMux()
mux.HandleFunc("POST /profiles", createProfileHandler(db))
mux.HandleFunc("GET /view", renderProfileHandler())
http.ListenAndServe(":8080", mux)
}Lo que esto demuestra:
DisallowUnknownFields para contratos de API estrictos.?, no formato de cadena.html/template auto-escapado para la visualización de parámetros de consulta.json.Unmarshal y json.Decoder mapean JSON a tipos Go; los valores cero rellenan los campos faltantes a menos que uses punteros para la detección opcional.go-playground/validator, funciones personalizadas) se ejecutan después de la decodificación para forzar formatos, rangos y reglas entre campos.DisallowUnknownFields rechaza claves JSON adicionales; útil para API públicas; a veces se relaja para API internas compatibles con versiones futuras.html/template maneja el texto de los elementos, mientras que los contextos JavaScript o URL necesitan reglas más estrictas o CSP.db.Query("SELECT * FROM users WHERE id = ?", id) nunca interpola cadenas de usuario en el texto de la consulta.| Capa | Verificación | Ejemplo |
|---|---|---|
| Transporte | Tamaño, tipo de contenido | MaxBytesReader, rechazar no application/json |
| Sintaxis | Éxito del análisis | json.Decoder.Decode, protobuf proto.Unmarshal |
| Esquema | Tipos, campos requeridos | Etiquetas de struct, DisallowUnknownFields |
| Semántica | Reglas de negocio | Formato de correo electrónico, lista de permitidos de enumeración, propiedad |
| Autorización | Quién puede establecer este campo | Eliminar is_admin de los DTO de creación públicos |
// Evita la asignación masiva: usa DTOs separados para crear vs. actualizar
type PublicCreateOrder struct {
SKU string `json:"sku"`
Quantity int `json:"quantity"`
// no Status, no UserID - el servidor los establece
}Age: -1 imposible o Role: "admin". Ejecuta siempre la validación semántica.is_admin, owner_id). Usa DTOs de entrada.html/template o devuelve solo JSON.fmt.Sprintf("SELECT * FROM t WHERE id = %s", id) es inyectable. Usa siempre marcadores de posición.err.Error() de los controladores de base de datos filtra pistas del esquema. Registra internamente; responde con mensajes genéricos."" y campo omitido ambos se convierten en "" en campos string. Usa punteros o sql.NullString cuando la distinción sea importante.| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
go-playground/validator etiquetas de struct | Grandes conjuntos de DTO con formatos estándar | Necesitas reglas de dominio que las etiquetas no pueden expresar |
| OpenAPI + generación de código | API públicas basadas en contrato | Manejadores internos pequeños donde la generación de código es pesada |
| Protobuf + protovalidate | gRPC con esquemas sólidos | Servicios REST simples solo JSON |
| WAF en la entrada | Firmas de ataque amplias | Sustituto de la validación a nivel de aplicación (nunca es suficiente por sí solo) |
Decoder procesa en streaming y admite DisallowUnknownFields por solicitud sin asignar todo el cuerpo crudo.
Unmarshal está bien para cuerpos pequeños después de haber limitado el tamaño con MaxBytesReader.
Habilítalo para API públicas versionadas donde los campos inesperados señalan errores del cliente o son intentos de sondeo.
Los servicios internos pueden permitir campos desconocidos para compatibilidad futura si documentas la política.
Léelos con r.URL.Query().Get o variables de ruta del mux, luego valida longitud, regex y listas de permitidos antes de usarlos.
Nunca pases segmentos de ruta crudos a comandos SQL o de shell.
Auto-escapa el texto de los elementos HTML.
Incrustar datos de usuario dentro de <script>, manejadores de eventos o URLs javascript: todavía requiere CSP y reglas de entrada estrictas.
Usa una forma fija como {"error":"bad request"} sin trazas de pila ni mensajes del controlador.
Registra el error real con el ID de solicitud en el servidor.
Protobuf impone tipos en el cable, no reglas de negocio.
Añade validación en los manejadores o usa protovalidate / verificaciones personalizadas después de la deserialización.
Valida las URLs contra una lista de permitidos de hosts y esquemas.
Bloquea IPs de enlace local, metadatos y rangos privados antes de http.Get.
La validación del cliente mejora la experiencia del usuario.
La validación del servidor es obligatoria: los clientes pueden ser eludidos por completo.
Limita el tamaño, detecta el tipo MIME por el contenido y no por la extensión, almacena fuera de la raíz web y escanea si la política lo requiere.
Nunca ejecutes contenido subido.
Normaliza y compara correos electrónicos y nombres de usuario con una política definida NFC/NFKC antes de las verificaciones de unicidad.
Documenta la regla de normalización para evitar casos de borde de cuentas duplicadas.
El regex captura basura obvia; la validación completa de correos electrónicos RFC es compleja.
Combina un regex razonable con flujos de confirmación para cuentas de alto riesgo.
Pruebas basadas en tablas con valores límite: vacíos, longitud máxima, enums inválidos, claves JSON desconocidas y metacaracteres SQL en campos de cadena.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (GC predeterminado de Green Tea, go fix modernizers - verificar parche en la compilación), chi (última versión - verificar en la compilación), gin (última versión - verificar en la compilación), echo (última versión - verificar en la compilación), google.golang.org/grpc (última versión - verificar en la compilación), sigs.k8s.io/controller-runtime (última versión - verificar en la compilación), kubebuilder (última versión - verificar en la compilación), tinygo (última versión - verificar objetivos de placa en la compilación), wazero (última versión - verificar en la compilación) y golangci-lint (última versión - verificar en la compilación).
Revisado por Chris St. John·Última actualización: 16 jul 2026