Skip to content

Security Review Baseline ​

What every change is checked against before it merges, by its author and by its reviewer. It covers what the CI scanners cannot see: the scanners find known-vulnerable modules, leaked secrets and bad Dockerfiles; they do not find a missing ownership check or a query built from a string.

MUST, SHOULD and MAY are used as in RFC 2119. Each section names the aioz-common package that already solves the problem; use it instead of writing your own.

Related baselines, not repeated here:

TopicWhere
Access tokens, refresh tokens, one-time codes, JWT, signing keystoken-security.md
Base images, non-root, digest pinning, build secretscontainer-images.md
govulncheck, gitleaks, trivy, hadolint and how to clear a findingci-cd.md
Migrator role, .env.migrate, deploy credentialsrelease.md

How a review uses this ​

  1. Read the whole diff, not only the files the merge request description mentions.
  2. For each changed file, go through the sections below that apply to it. An HTTP handler touches at least authorization, input and the HTTP surface; a store touches injection; a Dockerfile or compose file touches containers and deploy.
  3. Report each finding with its file and line, its severity, what an attacker does with it, and the fix.
  4. A finding at High or above blocks the merge. A Medium is fixed in the same merge request or gets a Jira subtask linked from it before merge.
SeverityMeaningExample
CriticalAnyone can reach data or control they should not, nowA secret committed; SQL injection on a public endpoint; an endpoint with no auth
HighA logged-in user can reach another user's data or act as them; a crash or exhaustion any client can triggerMissing ownership check; unbounded request body; SSRF to the internal network
MediumNeeds another weakness to be exploited, or exposes something minorStack trace in an error response; missing rate limit on a non-auth endpoint
LowHardeningA missing security header on an internal-only service

Changes touching auth, crypto or PII need two reviewers, and the merge request says so.


Secrets and configuration ​

  • Secrets MUST NOT appear in code, config files in git, images, CI logs, tickets, chat or application logs. A secret that reached any of them is compromised: rotate it first, then remove it and add the gitleaks fingerprint with a reason.
  • Secrets MUST come from the environment or from files outside the image (mode 0600, directory 0700), loaded at startup. Hold them in secret.Token so they print as redacted.
  • Configuration MUST be validated at startup, and the process MUST refuse to start on a missing or invalid security setting rather than fall back to an open default.
  • Default values MUST be safe: an empty allowlist allows nothing, an empty CORS origin list allows no origin, a debug or test flag defaults to off.
  • Test-only accounts, fixed codes or bypass flags MUST NOT exist in production config; where they exist at all they are behind config that is empty by default.
  • The debug listener (pprof, metrics, /health details) MUST bind to loopback or a private network only.

Authentication ​

  • Every endpoint MUST be authenticated unless it is on an explicit, reviewed list of public routes. Authentication is applied by router middleware for the whole group, never remembered per handler.
  • Tokens follow token-security.md.
  • Passwords MUST be hashed with aioz-common password (Argon2id), never a fast hash, and compared through its Verify.
  • Login, OTP verification, password reset and any "does this account exist" endpoint MUST be rate limited per account and per client IP, and MUST answer the same way for an unknown account as for a wrong secret.

Authorization ​

The most common serious finding, and the one no scanner catches.

  • Every request that names a resource by id MUST check that the caller may access that resource: the record's owner, tenant or organisation is compared with the caller's, in the query (WHERE id = $1 AND owner_id = $2) or immediately after loading it. A valid token is not permission.
  • The check MUST live in the service layer, so every transport (HTTP, gRPC, consumer) gets it.
  • List endpoints MUST scope by the caller in the query, never filter after loading.
  • Role and admin checks MUST be default-deny: an unknown role gets nothing.
  • The caller's identity MUST come from the verified token, never from a body field, query parameter or header the client controls (user_id, X-User-Id, is_admin).
  • Updates MUST bind to an explicit DTO of writable fields; never decode a request straight into a model that has owner_id, role, balance or status (mass assignment).
  • A not-found and a not-yours answer the same (404), so ids cannot be probed.
  • A test MUST prove that user B gets 404 for user A's resource, for every new endpoint that takes an id.

Input and limits ​

  • Every request body MUST be bounded (http.MaxBytesReader or the router's limit); a body over the limit is 413, never read to the end.
  • Every string, array and number in a DTO has a validated maximum length, size or range; enums are checked against their allowed values.
  • List endpoints paginate with a server-side maximum page size (see RESTfulAPI.md); a client cannot ask for everything.
  • The HTTP server MUST set ReadHeaderTimeout, ReadTimeout, WriteTimeout (or per-route deadlines for streaming) and IdleTimeout; Go's zero values mean no timeout.
  • A regular expression MUST NOT be compiled from user input.
  • Decoding refuses unknown fields where the API is strict, and never decodes into interface{} to switch on later.

Injection ​

  • SQL. Queries MUST be parameterized: pgx placeholders, gorm Where("x = ?", v), never fmt.Sprintf or string concatenation with a value. Identifiers that cannot be parameters (a sort column, a direction) MUST be mapped from an allowlist, never passed through. gorm Raw, Exec, Order, Group and Select with a string from the request are injection points.
  • Commands. exec.Command takes the program and its arguments separately; never sh -c with any part from input. An argument from input is preceded by -- where the program supports it.
  • Templates. HTML is rendered with html/template, never text/template; nothing user-provided is marked template.HTML.
  • Logs. Logging is structured (logger fields), so user input lands in a field and cannot forge a log line.
  • Headers and redirects. A redirect target from input MUST be checked against an allowlist of hosts or be a relative path; never echo input into a response header.

Outbound requests (SSRF) ​

  • Any request to a URL that came from a user, a webhook registration, a feed or an uploaded document MUST go through an aioz-common httpclient built with NewOneShot or with the private-network guard on, which refuses loopback, link-local (including the 169.254.169.254 metadata endpoint), RFC 1918 and other internal ranges after DNS resolution.
  • Every outbound client MUST set its dial, TLS handshake and overall timeouts explicitly when it is built in code: a zero httpclient.Config{} has none.
  • Outbound responses are read through a size limit.
  • TLS verification MUST NOT be disabled (InsecureSkipVerify) outside a test.

Files and paths ​

  • Uploads MUST go through aioz-common upload.Policy: a bounded size, an allowlist of types checked against the content and the extension, and the client's Content-Type ignored.
  • A filename from a client is never used as a path. Store under a generated name; show the original only through upload.DisplayName.
  • A path built from any input MUST be resolved inside a fixed root (os.Root, or filepath.Clean and a prefix check on the result) so ../ cannot leave it.
  • Archives are extracted with the same check per entry, and with limits on entry count and total size.

The HTTP surface ​

  • Every API wraps its router with aioz-common httpsec NewHeaders(...).Wrap, which sets the OWASP header set (HSTS, nosniff, frame denial, a strict default CSP, no-store).
  • CORS goes through httpsec.NewCORS with an explicit origin allowlist. * MUST NOT be combined with credentials, and an origin is never reflected back unchecked.
  • An endpoint authenticated by cookie MUST be protected against CSRF (SameSite=Lax or stricter, plus a token or an Origin check on state-changing requests). Cookies holding a session are Secure, HttpOnly and SameSite.
  • The client IP MUST come from aioz-common clientip, configured with the real trusted proxies; reading X-Forwarded-For directly lets any client choose its IP, and with it its rate-limit bucket.
  • Rate limits use aioz-common ratelimit (Redis-backed, shared across replicas), with an explicit OnError choice: closed for login and OTP, local or open elsewhere.
  • Responses with tokens, credentials, personal or financial data MUST use the no-store cache policy; a response that depends on the caller and may be stored is private with Vary: Authorization (RESTfulAPI.md).
  • Error responses go through apierr: a stable code and a message for the client, never a stack trace, SQL error, file path or internal host name. The detail goes to the log with the request id.

Data and privacy ​

  • Logs MUST NOT contain tokens, codes, passwords, keys, full card or bank numbers, or government ids; personal data (email, phone, address, precise location) is logged only where needed and never in bulk. Correlate by user id or request id instead.
  • An endpoint returns only the fields its DTO declares; a model is never serialized directly (it leaks hashes, internal flags and other users' ids).
  • Personal data collected is the minimum the feature needs, and the merge request says what is stored and for how long.
  • Traffic is TLS end to end where it leaves the host; a database or broker connection across hosts uses TLS.

Cryptography and randomness ​

  • Anything secret or unguessable (tokens, codes, ids that grant access, salts, nonces) comes from crypto/rand, through aioz-common secret or random; never math/rand.
  • Secrets and MACs are compared in constant time (subtle.ConstantTimeCompare, or the package that issued them).
  • No hand-rolled cryptography: no custom ciphers, no ECB, no reused nonces, no MD5 or SHA-1 for anything security-relevant.

Resources and denial of service ​

  • Every request, job and outbound call runs under a context with a deadline.
  • No unbounded growth from client input: maps, caches (use aioz-common lrucache), goroutines per request, queue payload sizes.
  • Expensive endpoints (search, export, report, anything that fans out) are paginated or asynchronous, and rate limited.
  • Consumers bound message size and refuse a payload they cannot parse instead of retrying it forever.

Dependencies ​

  • A new direct dependency is justified in the merge request: maintained (a release in the last year), widely used or reviewed, license compatible. Prefer aioz-common or the standard library.
  • No replace to a personal fork or a local path on main.
  • govulncheck findings are fixed by upgrading, never ignored (see ci-cd.md).

Containers and deploy ​

  • Images follow container-images.md: approved base, tag and digest, non-root 65532:65532, build secrets through BuildKit --secret.
  • Compose publishes data stores and debug ports on 127.0.0.1 only, unless the host is firewalled and the merge request says why.
  • Host files with secrets (.env, .env.migrate, config) are 0600 or 0640 with a group, never world-readable.
  • The migrator database role has DDL rights and is used only by deploy.sh migrate; application roles have DML only, on their own schema.
  • CI deploy variables are environment-scoped, protected and masked; SSH keys are written for the job and shredded in after_script.

Sources ​

  • OWASP Application Security Verification Standard 4.0.3, chapters V1-V14.
  • OWASP Top 10 2021, and the OWASP API Security Top 10 2023 (API1 object-level authorization, API3 property-level authorization, API4 resource consumption, API7 SSRF).
  • CWE Top 25 (2024).