How do I secure a Go web service?

Direct answer

Use `html/template` rather than `text/template` for anything rendered in a browser, set explicit timeouts on `http.Server` because the defaults are none, and pass query parameters as arguments rather than building SQL with `fmt.Sprintf`. Go's standard library is solid; the sharp edges are in what it doesn't do for you.

Muhammad HasanUpdated

html/template, not text/template

import "html/template"   // escapes, context-aware
import "text/template"   // does not escape

The APIs are near-identical, which is the problem. text/template renders user content into a page verbatim, and the code compiles and works. Until someone submits a script tag.

html/template escapes based on context, so a value in an attribute, in a URL and in a script block are each handled correctly.

template.HTML, template.JS and template.URL bypass escaping by design. Anywhere one wraps user input is a review trigger.

Set timeouts

srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       15 * time.Second,
    WriteTimeout:      15 * time.Second,
    IdleTimeout:       60 * time.Second,
}

http.ListenAndServe with default settings has no timeouts at all. A client that opens connections and sends headers slowly holds them indefinitely, the classic Slowloris shape, and it needs very little bandwidth to exhaust a server.

ReadHeaderTimeout is the one that closes that specific hole. Set all four.

Also cap request bodies with http.MaxBytesReader, or a large upload consumes memory unchecked.

SQL

database/sql parameterises when you pass arguments:

// Vulnerable
db.Query(fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email))

// Safe
db.Query("SELECT * FROM users WHERE email = $1", email)

The placeholder style depends on the driver. $1 for pq/pgx, ? for MySQL.

Ownership belongs in the query, as everywhere else:

db.QueryRow("SELECT * FROM orders WHERE id = $1 AND user_id = $2", id, userID)

Context and cancellation

Pass r.Context() into downstream calls. Without it, a client disconnecting leaves database queries and outbound requests running, which is both a resource leak and a way to amplify load.

Set timeouts on outbound HTTP too, http.DefaultClient has none, so a slow upstream can hold your handlers open indefinitely.

Errors

// Leaks internal detail
http.Error(w, err.Error(), 500)

// Better
log.Printf("handler error: %v", err)
http.Error(w, "internal error", 500)

Go errors frequently contain file paths, connection strings and query fragments. Log them, don't return them.

The rest

No CSRF protection in the standard library. Use gorilla/csrf or equivalent for cookie-authenticated browser traffic. No rate limiting either; golang.org/x/time/rate or a proxy in front.

Command execution is a good default: exec.Command doesn't invoke a shell unless you ask for one.

gosec is a reasonable free static analyser and catches the SQL, command execution and weak-crypto cases. Like everything else, it won't catch a missing ownership check.

Go deeper

Kolega for AppSec teams

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free