Security¶
Production-grade application security for senior Go backend engineers: authentication, authorization, token lifecycle, OAuth2/OIDC, TLS/mTLS, OWASP defenses, secrets management, and compliance.
34 questions across 13 topics · Level: senior
Topics¶
- Authentication vs Authorization (2)
- JWT Fundamentals (3)
- Token Revocation & Lifecycle (3)
- OAuth2 & OIDC (4)
- Sessions, Cookies & CSRF (3)
- HMAC & Webhook Security (2)
- TLS & mTLS (2)
- OWASP Top 10 & Access Control (2)
- Injection: SQL & Command (2)
- XSS, Input Validation & Mass Assignment (3)
- Password & Credential Storage (2)
- Secrets Management & Encryption (2)
- Rate Limiting, Hardening & Compliance (4)
Authentication vs Authorization¶
1. What is the difference between authentication and authorization, and why does conflating them cause security bugs?¶
Difficulty: 🟢 warm-up · Tags: authn, authz, access-control
Authentication answers "who are you?" — it verifies identity (credentials, a token, a client certificate). Authorization answers "are you allowed to do this?" — it checks whether the authenticated principal has permission for a specific action on a specific resource. The two are sequential: you authenticate first, then authorize each request. Conflating them produces classic bugs: a service trusts that any valid token means the caller may access any record, leading to broken access control / IDOR. A user with a perfectly valid JWT (authenticated) can fetch /orders/12345 belonging to someone else because the handler never re-checks ownership (authorization). The defense is to treat authentication as a gate at the edge and authorization as a per-resource decision in the handler, never assuming identity implies permission.
Key points - AuthN = identity verification; AuthZ = permission checking - AuthN happens once per request edge; AuthZ happens per resource/action - A valid token never implies access to a specific object - Missing object-level AuthZ is the root of IDOR/BOLA
Follow-ups - Where should authorization live: middleware, handler, or data layer? - How do you enforce authorization consistently across 50 microservices?
2. How do you model authorization for a multi-tenant e-commerce backend, and what are the trade-offs between RBAC, ABAC, and ReBAC?¶
Difficulty: 🟠 hard · Tags: authz, rbac, abac, rebac, multi-tenant
RBAC assigns permissions to roles (admin, seller, customer) and roles to users — simple, auditable, but coarse; it struggles with "seller can edit their own products." ABAC evaluates policies over attributes (user.tenant == resource.tenant && resource.status == 'draft') — flexible and good for ownership/context, but policies sprawl and become hard to reason about. ReBAC (Google Zanzibar / OpenFGA style) models authorization as a graph of relationships (user → owner-of → store → contains → product) — excellent for hierarchical, shareable resources but operationally heavier. In practice a multi-tenant store combines them: RBAC for coarse role gates, plus mandatory tenant scoping on every query (ABAC-style) so cross-tenant access is structurally impossible. The non-negotiable rule: object-level checks must run server-side on every read and write, never inferred from a role alone.
Key points - RBAC: roles→permissions, simple but coarse - ABAC: attribute/policy-based, handles ownership and context - ReBAC/Zanzibar: relationship graph, great for hierarchies & sharing - Always enforce tenant scoping in the data layer, not just middleware
// Tenant scoping: never trust the client-supplied tenant
func (r *OrderRepo) GetOrder(ctx context.Context, orderID string) (*Order, error) {
tenantID := auth.TenantFromContext(ctx) // from validated token, not request body
var o Order
err := r.db.QueryRowContext(ctx,
`SELECT id, total FROM orders WHERE id = $1 AND tenant_id = $2`,
orderID, tenantID,
).Scan(&o.ID, &o.Total)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound // same response as "not yours" to avoid leaking existence
}
return &o, err
}
Follow-ups - How do you prevent a 404-vs-403 oracle from leaking resource existence? - How would you centralize policy with OpenFGA or OPA?
JWT Fundamentals¶
3. Walk through the structure of a JWT and what each part guarantees.¶
Difficulty: 🟢 warm-up · Tags: jwt, tokens
A JWT is three Base64URL-encoded parts joined by dots: header.payload.signature. The header declares the type and signing algorithm (e.g. {"alg":"RS256","typ":"JWT"}). The payload holds claims — registered (iss, sub, aud, exp, iat, nbf, jti) and custom (roles, tenant). The signature is computed over base64url(header) + "." + base64url(payload) using the algorithm and key from the header. The signature guarantees integrity and authenticity — it proves the token was issued by the holder of the signing key and wasn't tampered with. It does not provide confidentiality: the payload is merely encoded, not encrypted, so anyone can read it. The critical consequence: never put secrets (passwords, PII, card data) in a JWT payload, and never trust an unverified token's claims.
Key points - Three parts: header.payload.signature, all Base64URL - Signature = integrity + authenticity, NOT confidentiality - Payload is readable by anyone — encode != encrypt - Registered claims: iss, sub, aud, exp, iat, nbf, jti
Follow-ups - When would you use a JWE (encrypted JWT) instead? - What does the jti claim enable?
4. Compare HS256 and RS256. When would you choose one over the other?¶
Difficulty: 🟡 medium · Tags: jwt, hs256, rs256, crypto
HS256 is HMAC with SHA-256 — a symmetric MAC. The same secret signs and verifies, so every party that can verify can also forge tokens. It's fast and simple, ideal when a single trusted service both issues and validates (monolith, or one auth service that shares no key). RS256 is RSA with SHA-256 — asymmetric. The auth server signs with a private key; any number of resource servers verify with the public key. They can verify but cannot mint tokens, which is essential in distributed systems and third-party integrations (the public key is published via JWKS). The trade-off: RS256 is slower and keys are larger, but it eliminates secret-sharing across services. Rule of thumb: HS256 for a single boundary, RS256 (or ES256 for smaller keys/faster verify) for multi-service or external verifiers.
Key points - HS256: symmetric HMAC — verifier can also forge - RS256: asymmetric — private signs, public verifies via JWKS - Distributed/3rd-party verifiers → RS256 (or ES256) - Single issuer+verifier → HS256 is fine and faster
// RS256 verification with key from JWKS, alg pinned
token, err := jwt.Parse(raw, func(t *jwt.Token) (interface{}, error) {
if t.Method.Alg() != "RS256" { // pin algorithm explicitly
return nil, fmt.Errorf("unexpected alg: %s", t.Method.Alg())
}
kid, _ := t.Header["kid"].(string)
return jwks.PublicKey(kid) // resolve by key id, supports rotation
}, jwt.WithExpirationRequired(), jwt.WithAudience("api.shop"), jwt.WithIssuer("https://auth.shop"))
Follow-ups - How does a key-confusion attack abuse a server that accepts both HS256 and RS256? - Why is ES256 often preferred over RS256 today?
5. What are the most dangerous JWT pitfalls and how do you defend against each?¶
Difficulty: 🟠 hard · Tags: jwt, alg-none, key-confusion, owasp
(1) alg:none — an attacker sets the header algorithm to none, strips the signature, and some libraries accept it as valid. Defense: never trust the header's alg; pin allowed algorithms server-side. (2) Algorithm confusion (RS256→HS256) — attacker takes the public RSA key, signs an HS256 token with it as the HMAC secret; a naive verifier uses the public key as a shared secret and accepts it. Defense: pin the expected algorithm, never derive verification mode from the token. (3) Missing exp — tokens live forever. Defense: require and validate exp (and nbf/iat). (4) Weak HMAC secret — brute-forceable offline. Defense: use a high-entropy secret (≥256 bits). (5) Sensitive data in payload — it's readable. Defense: keep only identifiers/claims, never secrets. (6) Not validating aud/iss — a token minted for service A is replayed at service B. Defense: validate audience and issuer.
Key points - Pin algorithms; never trust header.alg (alg:none, RS256→HS256 confusion) - Require and validate exp/nbf/iat - Use ≥256-bit HMAC secrets - No secrets/PII in payload (it's only encoded) - Always validate aud and iss
// Vulnerable: trusts whatever alg the token declares
jwt.Parse(raw, func(t *jwt.Token) (interface{}, error) {
return secretOrPubKey, nil // BUG: same key returned for HMAC and RSA paths
})
// Safe: explicit allowlist of methods
jwt.Parse(raw, keyFunc, jwt.WithValidMethods([]string{"RS256"}))
Follow-ups - How does pinning aud stop token replay across microservices? - Why is leeway for clock skew sometimes a security trade-off?
Token Revocation & Lifecycle¶
6. JWTs are stateless and self-validating. How do you revoke one before it expires?¶
Difficulty: 🟠 hard · Tags: jwt, revocation, denylist, refresh-tokens
Statelessness is the problem: a valid signature plus a future exp is accepted without a DB lookup, so you can't simply "delete" a token. Practical strategies: (1) Short TTL + refresh — issue access tokens with 5–15 min lifetimes; revocation is effectively waiting one TTL. The refresh token (long-lived, server-tracked) is what you actually revoke. (2) Denylist — store revoked jtis in Redis with TTL == token's remaining life; verify against it on each request. Reintroduces state but only for revoked tokens. (3) Token versioning — store a per-user token_version integer; embed it as a claim; bump it on logout/password-change to invalidate all existing tokens in one write. (4) Sessions in a fast store — abandon pure-stateless and look up a session each request. The standard production answer is short-TTL access tokens + revocable refresh tokens, optionally with a denylist for emergency revocation.
Key points - Can't revoke a stateless token directly — manage the refresh side - Short access TTL bounds the revocation window - Denylist revoked jti in Redis with matching TTL - token_version claim invalidates all of a user's tokens at once
// token_version check: bump in DB to kill all of a user's access tokens
claims := tok.Claims.(jwt.MapClaims)
userVer := userStore.TokenVersion(claims["sub"].(string))
if int(claims["ver"].(float64)) != userVer {
return ErrTokenRevoked
}
Follow-ups - What's the latency/cost trade-off of a per-request denylist check? - How do you propagate revocation across regions/edge caches?
7. Explain the roles of access vs refresh tokens and where each should be stored in a browser app.¶
Difficulty: 🟡 medium · Tags: tokens, access-token, refresh-token, storage, xss
The access token is short-lived (minutes), sent on every API call, and carries authorization claims. The refresh token is long-lived (days/weeks), sent only to the token endpoint to obtain new access tokens, and is the revocable, server-tracked credential. Splitting them limits blast radius: a leaked access token expires quickly; the powerful refresh token travels rarely. Storage is the contentious part. localStorage is readable by any JavaScript, so any XSS steals the token outright — avoid it for sensitive tokens. An httpOnly, Secure, SameSite cookie is invisible to JS (XSS can't read it) but is sent automatically, which reintroduces CSRF risk (mitigated by SameSite=Lax/Strict + anti-CSRF token). A common pattern: refresh token in an httpOnly cookie scoped to /auth/refresh, access token held in memory only (lost on reload, re-fetched via refresh). Never persist the refresh token in localStorage.
Key points - Access: short-lived, sent everywhere; Refresh: long-lived, sent rarely, revocable - localStorage is XSS-readable — avoid for tokens - httpOnly+Secure+SameSite cookie hides token from JS but invites CSRF - Best practice: refresh in httpOnly cookie, access in memory
Follow-ups - How do SameSite cookie modes interact with refresh-token CSRF? - What changes for a mobile/native client with no cookie jar?
8. What is refresh token rotation, and how does it enable theft detection?¶
Difficulty: 🟠 hard · Tags: refresh-token, rotation, theft-detection
Rotation means every time a refresh token is used, the server issues a new refresh token and invalidates the old one (one-time-use). This shrinks the useful lifetime of any single refresh token to a single use. The security win is theft detection via reuse: each refresh token belongs to a token family (a chain). If a previously-used (already-rotated) token is presented again, that's an anomaly — either the legitimate client and an attacker both hold a copy. The server can't tell which is which, so it revokes the entire family, forcing both to re-authenticate. This converts silent, long-lived token theft into a detectable event. Implementation: store each refresh token (hashed) with a family_id and used flag; on reuse of a consumed token, invalidate all tokens sharing that family_id. Pair with short access TTLs so the attacker's window is minimal.
Key points - Rotation = one-time-use refresh tokens, new one issued each refresh - Reuse of a rotated token signals theft (legit client + attacker both have it) - On reuse, revoke the whole token family - Store hashed tokens with family_id and used flag
// On refresh: detect reuse, revoke family
rt := store.Lookup(hash(presented))
if rt == nil { return ErrInvalid }
if rt.Used { // already rotated -> reuse detected
store.RevokeFamily(rt.FamilyID)
return ErrTokenReuseRevoked
}
rt.Used = true; store.Save(rt)
newRT := store.Issue(rt.UserID, rt.FamilyID) // same family, fresh token
Follow-ups - How do you handle race conditions when a client retries a refresh? - Should hashing the refresh token use bcrypt or HMAC/SHA-256, and why?
OAuth2 & OIDC¶
9. Describe the OAuth2 authorization code flow with PKCE and what PKCE protects against.¶
Difficulty: 🟠 hard · Tags: oauth2, pkce, authorization-code
In the authorization code flow the client redirects the user to the authorization server, the user authenticates and consents, and the server redirects back with a short-lived authorization code. The client then exchanges that code at the token endpoint for tokens — over a back channel, so tokens never appear in the browser URL/history. PKCE (Proof Key for Code Exchange) hardens this for public clients (SPAs, mobile) that can't keep a secret. The client generates a random code_verifier, sends code_challenge = SHA256(verifier) with the authorization request, then sends the raw verifier during the token exchange. The server checks the hash matches. This defeats authorization code interception: even if an attacker steals the code (e.g. via a malicious app registering the same redirect URI, or a logged URL), they can't redeem it without the verifier, which never left the client. PKCE is now recommended for all clients, including confidential ones.
Key points - Code returned via redirect; tokens exchanged on a back channel - PKCE: code_challenge=SHA256(verifier) on auth request, verifier on exchange - Defeats authorization-code interception for public clients - Recommended for all clients, not just SPAs/mobile
Follow-ups - Why is the state parameter still needed alongside PKCE? - What's the difference between plain and S256 challenge methods?
10. When do you use the client credentials grant, and why was the implicit grant deprecated?¶
Difficulty: 🟡 medium · Tags: oauth2, client-credentials, implicit, deprecated
Client credentials is the machine-to-machine grant: no user is involved. A confidential client (a backend service) authenticates with its own client ID/secret (or mTLS) and receives an access token representing itself, used for service-to-service APIs, cron jobs, and daemons. There's no user identity, so no refresh token and no ID token. The implicit grant returned the access token directly in the redirect URI fragment (#access_token=...) to avoid a back-channel exchange in browsers that lacked CORS. It's deprecated because the token is exposed in the URL — leaking via browser history, referer headers, and logs — with no client authentication and no clean way to deliver a refresh token. Modern guidance (OAuth 2.1) replaces it entirely with authorization code + PKCE, which keeps tokens off the front channel. Treat any new use of implicit as a red flag.
Key points - Client credentials = M2M, no user, no refresh/ID token - Implicit returned tokens in the URL fragment — leaky - No client auth, token exposed in history/referer/logs - OAuth 2.1: use auth code + PKCE instead of implicit
Follow-ups - How do you authenticate the client in client-credentials securely (secret vs private_key_jwt vs mTLS)? - What scopes/audience should an M2M token carry?
11. What is the difference between OAuth2 and OIDC, and what does the ID token add?¶
Difficulty: 🟡 medium · Tags: oidc, oauth2, id-token, authentication
OAuth2 is an authorization framework — it issues access tokens that grant a client delegated access to resources. It deliberately says nothing about who the user is; the access token is opaque to the client and meant for the resource server. People misused OAuth2 for login by inferring identity from API calls, which is fragile and insecure. OIDC (OpenID Connect) is an identity layer on top of OAuth2 that standardizes authentication. It adds the ID token — a JWT with verified claims about the user (sub, email, name, iss, aud, exp, nonce) intended for the client to establish a login session. It also defines the /userinfo endpoint and discovery (.well-known/openid-configuration). Rule: use the access token to call APIs, use the ID token to know who logged in. Never use an access token as proof of identity, and always validate the ID token's signature, aud, iss, and nonce.
Key points - OAuth2 = authorization (access tokens for APIs) - OIDC = authentication layer on OAuth2 - ID token (JWT) carries verified user identity for the client - Validate ID token sig, aud, iss, nonce; don't use access token for identity
Follow-ups - What's the purpose of the nonce in the ID token? - Why should the client never call /userinfo with an ID token?
12. How does a tool like Keycloak fit into an OAuth2/OIDC architecture, and what does centralizing it buy you?¶
Difficulty: 🟡 medium · Tags: keycloak, oidc, iam, sso
Keycloak is an identity and access management (IAM) server — a self-hosted OAuth2/OIDC authorization server plus user federation. It centralizes authentication so your services never handle passwords: they redirect to Keycloak, which authenticates the user (local DB, LDAP/AD, or social/federated IdPs), issues OIDC ID tokens and OAuth2 access tokens signed with its keys, and publishes a JWKS endpoint for verification. Your Go services become resource servers: they fetch Keycloak's public keys, validate incoming JWTs (signature, iss, aud, exp), and read realm/client roles from the token to authorize. Centralizing buys you single sign-on, consistent MFA/password policy, token rotation and key management in one place, fine-grained roles/groups, and the ability to revoke sessions globally. The trade-off is operational: Keycloak becomes a critical, high-availability dependency, so you cache JWKS, plan key rotation, and run it redundantly.
Key points - Keycloak = self-hosted OAuth2/OIDC IAM + user federation - Services become resource servers validating JWTs against its JWKS - Centralizes SSO, MFA, roles, key/token management - Becomes a critical HA dependency — cache JWKS, plan rotation
Follow-ups - How do you map Keycloak realm/client roles to your authorization model? - How do you handle Keycloak key rotation without an outage?
Sessions, Cookies & CSRF¶
13. Contrast session-based and token-based authentication. What are the trade-offs?¶
Difficulty: 🟡 medium · Tags: sessions, tokens, authentication
Session-based auth stores state server-side: on login the server creates a session record and sends an opaque session ID in a cookie. Each request the server looks up the session. It's trivially revocable (delete the session), the cookie carries no data, and it's well understood — but it needs shared/sticky session storage to scale horizontally and is cookie-bound (CSRF-prone). Token-based (JWT) auth is stateless: the token itself carries claims and is self-validating, so any service with the key can authorize without a central store — great for microservices and APIs. The cost is revocation (covered earlier), token bloat, and the storage/XSS questions. Pragmatic guidance: for a classic server-rendered web app, sessions are simpler and safer; for distributed APIs and SPAs/mobile consuming many services, tokens win. Many systems use a hybrid: stateless access tokens for APIs, server-tracked refresh tokens for revocation.
Key points - Sessions: server-side state, opaque cookie, easy revocation, needs shared store - Tokens: stateless, self-validating, scale across services, hard to revoke - Sessions are cookie-bound → CSRF; tokens-in-header → mostly CSRF-immune - Hybrid: stateless access + revocable refresh is common
Follow-ups - How do you scale server-side sessions across many instances? - Why are tokens sent in the Authorization header largely immune to CSRF?
14. Explain CSRF, and why an API that authenticates via a bearer token in the Authorization header is mostly immune.¶
Difficulty: 🟠 hard · Tags: csrf, cookies, bearer-token
CSRF (Cross-Site Request Forgery) abuses the browser's habit of automatically attaching cookies to any request to a domain, regardless of which site initiated it. If you're logged in to bank.com (session cookie) and visit evil.com, a hidden form or image can fire a state-changing request to bank.com; the browser attaches your cookie and the server can't tell it wasn't you. The attack relies entirely on ambient/automatic credentials. A bearer token in the Authorization header is not sent automatically — JavaScript must explicitly add it, and cross-origin JS can't read your token (it's in memory or a different origin's storage) thanks to the same-origin policy and CORS. So a forged cross-site request simply arrives with no Authorization header and is rejected. The caveat: this immunity disappears if you store the token in a cookie that's auto-sent — then you're back to CSRF and need defenses.
Key points - CSRF exploits auto-attached cookies on cross-site requests - Requires ambient credentials (cookies), not explicit headers - Authorization: Bearer is added by JS, not auto-sent → immune - Putting the token in an auto-sent cookie reintroduces CSRF
Follow-ups - Does CORS prevent CSRF? Why or why not? - How does a login CSRF differ from a classic CSRF?
15. What are the main CSRF defenses, and how do SameSite cookies and anti-CSRF tokens compare?¶
Difficulty: 🟡 medium · Tags: csrf, samesite, csrf-token, cookies
SameSite cookies instruct the browser when to send a cookie cross-site. SameSite=Strict never sends it on cross-site requests (safest, but breaks inbound links to authenticated pages). SameSite=Lax (a sensible default) sends it on top-level GET navigations but not on cross-site POST/embedded requests, blocking most CSRF. SameSite=None (requires Secure) sends it everywhere — needed for legitimate cross-site contexts but offers no CSRF protection alone. Anti-CSRF tokens are an explicit defense: the server issues an unpredictable token (synchronizer token, or the double-submit-cookie pattern) that must accompany state-changing requests in a header or form field; cross-site attackers can't read or guess it. Best practice is defense-in-depth: set SameSite=Lax (or Strict) and require an anti-CSRF token for cookie-authenticated state changes, plus validate Origin/Referer. For header-token APIs you typically need none of this.
Key points - SameSite Strict/Lax/None controls cross-site cookie sending - Lax is a good default; None offers no CSRF protection - Anti-CSRF tokens: synchronizer or double-submit, unguessable per request - Defense-in-depth: SameSite + CSRF token + Origin check
http.SetCookie(w, &http.Cookie{
Name: "session", Value: id,
HttpOnly: true, Secure: true,
SameSite: http.SameSiteLaxMode, // blocks most cross-site POST CSRF
Path: "/",
})
Follow-ups - How does the double-submit cookie pattern work and when does it fail? - Why isn't SameSite alone sufficient for older browsers?
HMAC & Webhook Security¶
16. How do you securely verify an incoming webhook, and why is HMAC the standard mechanism?¶
Difficulty: 🟡 medium · Tags: hmac, webhook, signature, constant-time
A webhook is an unauthenticated inbound POST from a third party (Stripe, GitHub), so you must verify it really came from the claimed sender and wasn't altered. The standard is an HMAC signature: the sender computes HMAC-SHA256(shared_secret, raw_request_body) (often including a timestamp) and sends it in a header (e.g. X-Signature). You recompute the HMAC over the raw bytes you received and compare. HMAC is preferred because it's a keyed MAC — only holders of the shared secret can produce a valid signature, giving both authenticity and integrity with cheap symmetric crypto. Two critical implementation details: compute over the exact raw body (re-serializing JSON changes bytes and breaks the signature), and compare using a constant-time function (hmac.Equal) to avoid timing side channels. Some providers sign with RSA/ECDSA instead, in which case you verify with their public key.
Key points - HMAC-SHA256 over raw body proves authenticity + integrity - Only the shared-secret holder can sign - Verify over exact raw bytes — never re-serialized JSON - Use constant-time comparison (hmac.Equal)
func verifyWebhook(body []byte, sigHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := mac.Sum(nil)
got, err := hex.DecodeString(sigHeader)
if err != nil { return false }
return hmac.Equal(expected, got) // constant-time
}
Follow-ups - Why must you read the raw body before any JSON middleware consumes it? - What goes wrong if you use bytes.Equal instead of hmac.Equal?
17. A valid signed webhook can still be captured and re-sent. How do you prevent replay attacks?¶
Difficulty: 🟠 hard · Tags: webhook, replay, nonce, timestamp, idempotency
A correct HMAC only proves the message is authentic and unmodified — it says nothing about freshness, so an attacker who captures a valid signed request can replay it (e.g. re-trigger a 'payment succeeded' event). Two complementary defenses: (1) Signed timestamps — include a timestamp in the signed payload and reject requests whose timestamp is outside a tight window (e.g. ±5 minutes). Because the timestamp is part of the HMAC input, an attacker can't move it forward without invalidating the signature. This bounds the replay window but doesn't stop replays within it. (2) Nonces / idempotency keys — record each unique event ID (or nonce) you've processed and reject duplicates. This makes processing exactly-once even within the time window. In practice you combine them: validate the signature, reject stale timestamps, then deduplicate by event ID stored in a fast store with a TTL slightly longer than the timestamp window. Idempotent handlers are the safety net against any replay that slips through.
Key points - HMAC proves authenticity, not freshness — replays are still valid - Sign a timestamp and reject outside a tight window (±5 min) - Track nonces/event IDs to reject duplicates (idempotency) - Combine: signature + timestamp window + dedup store with TTL
Follow-ups - Why include the timestamp inside the signed payload rather than a separate header? - How long should the dedup store TTL be relative to the timestamp tolerance?
TLS & mTLS¶
18. Walk through what a TLS handshake establishes and the role of the certificate chain of trust.¶
Difficulty: 🟠 hard · Tags: tls, handshake, certificates, chain-of-trust
A TLS handshake authenticates the server and negotiates an encrypted, integrity-protected channel. Simplified (TLS 1.3): client and server agree on a cipher suite, exchange ephemeral Diffie-Hellman key shares to derive a shared session key (giving forward secrecy — past traffic stays safe even if the server's long-term key later leaks), and the server presents its certificate. The certificate binds the server's public key to its domain and is signed by a Certificate Authority. The client verifies the chain of trust: the leaf cert is signed by an intermediate CA, which is signed (transitively) by a root CA in the client's trust store. The client checks each signature up the chain, the validity dates, that the hostname matches the cert's SAN, and revocation status (OCSP/CRL). Only if the chain anchors in a trusted root and all checks pass does the handshake complete. This is what prevents an attacker from presenting a forged certificate for your domain.
Key points - Handshake authenticates server + derives a shared session key - Ephemeral DH gives forward secrecy - Cert binds public key to domain, signed by a CA - Chain of trust: leaf → intermediate → trusted root; verify sigs, dates, SAN, revocation
Follow-ups - What does forward secrecy protect against and how does TLS 1.3 ensure it? - How does certificate pinning change the trust model and its risks?
19. What is mTLS, why use it for service-to-service traffic, and how do you handle certificate rotation?¶
Difficulty: 🟠 hard · Tags: mtls, service-to-service, zero-trust, cert-rotation
In normal TLS only the server proves its identity. Mutual TLS (mTLS) adds client authentication: the client also presents a certificate, and the server validates it against a trusted CA. For internal service-to-service traffic this gives strong, cryptographic identity for both peers — service A knows it's really talking to B, and B knows the caller is A — without bearer tokens that can be stolen and replayed. It underpins zero-trust networking (no implicit trust from being 'inside' the network) and is the basis of service meshes (Istio/Linkerd). The operational challenge is certificate rotation: internal certs should be short-lived (hours/days) to limit exposure, which makes manual rotation infeasible. You automate issuance and renewal with a workload identity system — SPIFFE/SPIRE, Vault PKI, or a mesh's built-in CA — that mints short-lived certs and rotates them transparently. Key practices: short TTLs, automated renewal before expiry, graceful reload without dropping connections, and keeping a current trust bundle so rotating the CA doesn't cause an outage.
Key points - mTLS = both client and server present and validate certs - Strong mutual identity for service-to-service, basis of zero-trust/mesh - Replaces stealable bearer tokens for internal auth - Rotate short-lived certs automatically (SPIFFE/SPIRE, Vault PKI, mesh CA)
Follow-ups - How does SPIFFE/SPIRE issue and rotate workload identities? - How do you rotate the issuing CA without breaking in-flight connections?
OWASP Top 10 & Access Control¶
20. Name several OWASP Top 10 categories relevant to a Go backend and give a concrete example of each.¶
Difficulty: 🟡 medium · Tags: owasp, access-control, injection, ssrf
A01 Broken Access Control — e.g. IDOR/BOLA: /users/{id}/card returns any user's card because ownership isn't checked. A02 Cryptographic Failures — storing passwords as unsalted SHA-1, or PII unencrypted at rest. A03 Injection — SQL/NoSQL/command injection from concatenating user input into queries. A04 Insecure Design — flaws baked into the design, like no rate limiting on password reset. A05 Security Misconfiguration — debug endpoints exposed, default credentials, permissive CORS (Access-Control-Allow-Origin: * with credentials), verbose error stack traces leaking internals. A07 Identification & Authentication Failures — no brute-force protection, weak session management. A08 Software & Data Integrity Failures — unsigned updates, deserializing untrusted data, compromised dependencies. A09 Logging & Monitoring Failures — no audit trail to detect/respond to breaches. A10 SSRF — a service fetches a user-supplied URL, letting attackers reach internal metadata endpoints. Broken access control and injection are consistently the highest-impact for backends.
Key points - A01 Broken Access Control (IDOR/BOLA) — top backend risk - A03 Injection — SQL/command from concatenated input - A05 Misconfiguration — exposed debug, permissive CORS, verbose errors - A08 Integrity, A09 Logging, A10 SSRF round out backend concerns
Follow-ups - How would you systematically test for BOLA in an API with hundreds of endpoints? - What internal targets make SSRF dangerous in cloud environments?
21. Explain IDOR/BOLA and the correct defense. Why is obscuring the ID not enough?¶
Difficulty: 🟠 hard · Tags: idor, bola, access-control, owasp
IDOR (Insecure Direct Object Reference) — also called BOLA (Broken Object Level Authorization) in API contexts — is when an endpoint exposes a reference to an object (an ID) and authorizes based only on authentication, not on whether this user owns this object. Authenticated user A requests /orders/1002 (B's order) and gets it because the handler trusts the ID. The correct defense is a server-side object-level authorization check on every access: scope the query to the caller's identity (WHERE id=$1 AND owner_id=$2) or explicitly verify ownership/permission before returning data. Crucially, using random UUIDs instead of sequential IDs is not a fix — that's security through obscurity. The IDs still appear in URLs, logs, referer headers, and shared links, and any leaked or guessed ID grants access. Unpredictable IDs reduce enumeration but do nothing once an attacker holds a valid reference. The authorization check is mandatory; non-sequential IDs are only a minor, complementary hardening.
Key points - IDOR/BOLA = authorizing by authentication, not object ownership - Fix: server-side ownership/permission check on every object access - Scope queries to the caller (WHERE owner_id = current_user) - Random UUIDs are obscurity, not a fix — IDs leak
Follow-ups - How do you avoid leaking existence via 403 vs 404 responses? - Where should the ownership check live to be consistent across endpoints?
Injection: SQL & Command¶
22. How do parameterized queries prevent SQL injection, and why is escaping/sanitizing input not a reliable defense?¶
Difficulty: 🟠 hard · Tags: sql-injection, parameterized-queries, injection, owasp
SQL injection happens when user input is concatenated into a query string, so the input can change the query's structure (' OR '1'='1, '; DROP TABLE users;--). Parameterized queries / prepared statements fix this at the root: the SQL text with placeholders ($1, ?) is sent to and parsed by the database separately from the parameter values. The query plan is fixed before any data arrives, so parameters are always treated as data, never as code — there is no string for the attacker's input to break out of. Manual escaping is unreliable because it tries to enumerate and neutralize every dangerous character across every context, encoding, and DB dialect; you miss edge cases (multibyte/charset tricks, quoting in unexpected contexts, numeric/identifier positions where quotes don't even apply). Escaping is a blacklist that's only as good as its completeness. Always parameterize. For things that can't be parameterized (table/column names, ORDER BY), use a strict allowlist of permitted values, never raw input.
Key points - Injection alters query structure via concatenated input - Parameterized queries separate SQL text (parsed first) from data - Parameters are always data, never executable — no breakout - Escaping is an incomplete blacklist; allowlist identifiers that can't be parameterized
// Vulnerable: string concatenation
q := "SELECT * FROM users WHERE email = '" + email + "'" // INJECTION
// Safe: parameterized
row := db.QueryRowContext(ctx,
"SELECT id, name FROM users WHERE email = $1", email)
// Dynamic ORDER BY can't be parameterized -> allowlist
var allowed = map[string]string{"name": "name", "date": "created_at"}
col, ok := allowed[sortKey]
if !ok { return ErrBadSort }
Follow-ups - Does an ORM make you immune, and how can you still get injection through one? - How would you handle a dynamic IN-clause with a variable number of values?
23. Beyond SQL, what other injection classes should a Go backend defend against, and how?¶
Difficulty: 🟡 medium · Tags: command-injection, nosql-injection, path-traversal, injection
OS command injection — building shell commands from input (exec.Command("sh", "-c", "convert "+name)). Defense: never invoke a shell; call binaries directly with exec.Command(prog, args...) so arguments aren't reparsed, and allowlist values. NoSQL injection — passing user-controlled structures into Mongo/etc. where operators ($gt, $ne) sneak in. Defense: validate types, bind to concrete structs, don't pass raw maps from request bodies into queries. LDAP injection — filters built from input; use proper escaping/parameterized filter APIs. Header/log injection (CRLF) — input containing \r\n splits responses or forges log lines; sanitize/encode before writing to headers or logs. Template injection — user input executed by a template engine; in Go, html/template auto-contextually escapes, but never build templates from user input. Path traversal — ../ in file paths; clean and confine paths to a base dir. The unifying principle: keep untrusted data as data, use structured/typed APIs over string-building, and validate at trust boundaries.
Key points - Command injection: avoid shell, use exec.Command with arg list - NoSQL injection: bind to structs, validate types, no raw maps - Log/header CRLF injection: sanitize input before writing - Path traversal: clean and confine to a base directory - Principle: structured/typed APIs over string concatenation
// Vulnerable: shell reparses the string
exec.Command("sh", "-c", "ping "+host).Run() // INJECTION
// Safe: direct exec, args not reparsed by a shell
exec.Command("ping", "-c", "1", host).Run()
Follow-ups - Why does passing args as a slice to exec.Command avoid injection? - How does filepath.Clean alone fail to stop traversal, and what else is needed?
XSS, Input Validation & Mass Assignment¶
24. Explain the XSS types and why context-aware output encoding plus CSP is the right defense.¶
Difficulty: 🟠 hard · Tags: xss, output-encoding, csp, owasp
XSS injects attacker-controlled script into a page so it runs in victims' browsers, stealing tokens/cookies or acting as the user. Stored XSS persists the payload server-side (e.g. a malicious comment) and serves it to everyone. Reflected XSS bounces input straight back in a response (search term echoed unescaped). DOM-based XSS happens entirely client-side when JS writes untrusted data into the DOM (innerHTML). The primary defense is context-aware output encoding: escape data according to where it lands — HTML body, attribute, JavaScript, URL, and CSS each need different encoding. Go's html/template does this automatically and contextually, which is why you should render with it rather than concatenating HTML. Input validation complements but doesn't replace encoding (you can't reject all valid-but-dangerous input). CSP (Content-Security-Policy) is defense-in-depth: a restrictive policy (default-src 'self', no inline scripts, nonce/hash for allowed scripts) means even if a payload is injected, the browser refuses to execute it. Combine: encode on output, validate on input, and ship a strict CSP.
Key points - Stored, reflected, DOM-based XSS - Primary defense: context-aware output encoding (HTML/attr/JS/URL/CSS) - Go html/template auto-escapes contextually — use it - CSP is defense-in-depth: blocks execution even if injected - Input validation complements, never replaces, output encoding
Follow-ups - Why does encoding depend on context, and what breaks if you HTML-escape inside a