@kaademos/secure-sdlc 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ # Go Security Profile
2
+
3
+ **Language:** Go 1.22+
4
+ **Frameworks:** net/http (stdlib), Gin, Echo, Fiber
5
+ **ASVS Baseline:** L2
6
+
7
+ ---
8
+
9
+ ## Go's Standard Library Is Safe — Until You Reach Around It
10
+
11
+ Go's stdlib gives you memory safety, `html/template` context-aware escaping, and
12
+ `database/sql` placeholders for free. Almost every Go web vulnerability comes from
13
+ *opting out* of these: using `text/template` for HTML, building SQL with `fmt.Sprintf`,
14
+ running shell strings through `exec.Command("sh", "-c", ...)`, or enabling a wildcard CORS
15
+ policy alongside credentials. This profile focuses on those opt-out traps.
16
+
17
+ ---
18
+
19
+ ## XSS — Use `html/template`, Never `text/template`, for HTML
20
+
21
+ `html/template` escapes output based on context (HTML body, attribute, JS, URL, CSS).
22
+ `text/template` performs **zero** escaping and must never render attacker-influenced HTML.
23
+
24
+ ```go
25
+ import (
26
+ "html/template" // ✓ context-aware auto-escaping
27
+ // "text/template" // ✗ NO escaping — XSS if used for HTML
28
+ )
29
+
30
+ // ✓ Safe — html/template escapes .Name in HTML context
31
+ var tmpl = template.Must(template.New("p").Parse(`<p>Hello {{ .Name }}</p>`))
32
+ tmpl.Execute(w, map[string]string{"Name": userInput}) // <script> becomes &lt;script&gt;
33
+ ```
34
+
35
+ ```go
36
+ // ✗ Unsafe — template.HTML tells the engine "this is already safe", disabling escaping
37
+ tmpl.Execute(w, map[string]any{"Bio": template.HTML(user.Bio)}) // stored XSS
38
+
39
+ // ✓ Safe — sanitise untrusted HTML first, then mark trusted
40
+ import "github.com/microcosm-cc/bluemonday"
41
+
42
+ p := bluemonday.UGCPolicy()
43
+ safe := template.HTML(p.Sanitize(user.Bio)) // allow-list of tags/attrs only
44
+ ```
45
+
46
+ **Other escaping bypasses to flag in review:** `template.JS`, `template.URL`,
47
+ `template.CSS`, `template.HTMLAttr` — each disables escaping for that context. They are
48
+ only safe with values your code fully controls, never with user input.
49
+
50
+ > Framework note: Gin's `c.HTML()`, Echo's `c.Render()`, and Fiber's `html/v2` engine all
51
+ > build on `html/template`, so context escaping applies. Setting `Content-Type: text/html`
52
+ > by hand and writing a `fmt.Sprintf`'d string with `c.String()`/`w.Write()` bypasses it.
53
+
54
+ ---
55
+
56
+ ## SQL Injection — Placeholders, Never `fmt.Sprintf`
57
+
58
+ ### `database/sql`
59
+
60
+ ```go
61
+ // ✗ SQL injection — string formatting builds the query
62
+ q := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
63
+ rows, err := db.Query(q)
64
+
65
+ // ✓ Parameterised — driver sends value separately from SQL
66
+ rows, err := db.Query("SELECT * FROM users WHERE email = $1", email) // pq / pgx
67
+ rows, err := db.Query("SELECT * FROM users WHERE email = ?", email) // MySQL / SQLite
68
+ err := db.QueryRow("SELECT id FROM users WHERE email = $1", email).Scan(&id)
69
+ ```
70
+
71
+ Identifiers (table/column names, `ORDER BY`) **cannot** be parameterised — allow-list them:
72
+
73
+ ```go
74
+ // ✗ Unsafe — attacker controls the ORDER BY clause
75
+ db.Query("SELECT * FROM users ORDER BY " + r.URL.Query().Get("sort"))
76
+
77
+ // ✓ Safe — validate against a fixed set before interpolating
78
+ var allowedSort = map[string]string{"name": "name", "created": "created_at"}
79
+ col, ok := allowedSort[r.URL.Query().Get("sort")]
80
+ if !ok {
81
+ col = "created_at"
82
+ }
83
+ db.Query("SELECT * FROM users ORDER BY " + col) // col is now a known-safe literal
84
+ ```
85
+
86
+ ### GORM
87
+
88
+ ```go
89
+ // ✓ Safe — ? placeholders are escaped by GORM
90
+ db.Where("email = ?", email).First(&user)
91
+ db.Raw("SELECT * FROM users WHERE email = ?", email).Scan(&user)
92
+
93
+ // ✗ Unsafe — Sprintf into the condition string defeats GORM's escaping
94
+ db.Where(fmt.Sprintf("email = '%s'", email)).First(&user)
95
+
96
+ // ✗ Unsafe — user-controlled column name in a struct/map update can corrupt other fields
97
+ db.Model(&user).Updates(userControlledMap) // allow-list keys, or use a typed struct
98
+ ```
99
+
100
+ ---
101
+
102
+ ## CORS — Never Wildcard Origin *with* Credentials
103
+
104
+ `Access-Control-Allow-Origin: *` combined with `Allow-Credentials: true` is rejected by
105
+ browsers, so the usual "fix" is reflecting the request `Origin` — which silently allows
106
+ **every** site. Always use an explicit allow-list.
107
+
108
+ ```go
109
+ // stdlib via github.com/rs/cors
110
+ import "github.com/rs/cors"
111
+
112
+ // ✗ Unsafe — allows any origin to make credentialed requests
113
+ c := cors.New(cors.Options{
114
+ AllowedOrigins: []string{"*"},
115
+ AllowCredentials: true,
116
+ })
117
+
118
+ // ✓ Safe — explicit origins, credentials only for those
119
+ c := cors.New(cors.Options{
120
+ AllowedOrigins: []string{"https://app.example.com"},
121
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
122
+ AllowCredentials: true,
123
+ MaxAge: 300,
124
+ })
125
+ handler := c.Handler(mux)
126
+ ```
127
+
128
+ ```go
129
+ // Gin — github.com/gin-contrib/cors
130
+ import "github.com/gin-contrib/cors"
131
+
132
+ // ✗ Unsafe — AllowAllOrigins + credentials reflects every Origin
133
+ r.Use(cors.New(cors.Config{AllowAllOrigins: true, AllowCredentials: true}))
134
+
135
+ // ✓ Safe — explicit list
136
+ r.Use(cors.New(cors.Config{
137
+ AllowOrigins: []string{"https://app.example.com"},
138
+ AllowCredentials: true,
139
+ }))
140
+ ```
141
+
142
+ ```go
143
+ // Echo — middleware.CORSWithConfig (AllowOrigins) and
144
+ // Fiber — github.com/gofiber/fiber/v2/middleware/cors (AllowOrigins) follow the same rule:
145
+ // list real origins; never set AllowOrigins:"*" together with AllowCredentials:true.
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Security Headers — Stdlib Has None
151
+
152
+ `net/http` sends no security headers. Add them via middleware on every response.
153
+
154
+ ```go
155
+ func secureHeaders(next http.Handler) http.Handler {
156
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157
+ h := w.Header()
158
+ h.Set("Content-Security-Policy", "default-src 'self'")
159
+ h.Set("X-Content-Type-Options", "nosniff")
160
+ h.Set("X-Frame-Options", "DENY")
161
+ h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
162
+ h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
163
+ next.ServeHTTP(w, r)
164
+ })
165
+ }
166
+ // Or use github.com/unrolled/secure for a configurable equivalent.
167
+ // Gin: gin-contrib/secure · Echo: middleware.Secure() · Fiber: middleware/helmet
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Authentication & Passwords
173
+
174
+ ```go
175
+ // ✓ Password hashing — bcrypt (cost ≥ 12) or argon2id
176
+ import "golang.org/x/crypto/bcrypt"
177
+
178
+ hash, err := bcrypt.GenerateFromPassword([]byte(pw), 12)
179
+ err = bcrypt.CompareHashAndPassword(hash, []byte(pw)) // constant-time
180
+
181
+ // ✗ Never — fast/unsalted hashes for passwords
182
+ sum := sha256.Sum256([]byte(pw)) // brute-forceable
183
+ ```
184
+
185
+ ```go
186
+ // ✓ Constant-time comparison for tokens/HMACs — avoid timing leaks
187
+ import "crypto/subtle"
188
+
189
+ if subtle.ConstantTimeCompare(got, want) == 1 { /* match */ }
190
+
191
+ // ✗ Unsafe — == short-circuits and leaks length/prefix via timing
192
+ if string(got) == string(want) { /* ... */ }
193
+ ```
194
+
195
+ JWT: pin the expected algorithm in the keyfunc to defeat `alg: none` / algorithm-confusion:
196
+
197
+ ```go
198
+ token, err := jwt.Parse(raw, func(t *jwt.Token) (any, error) {
199
+ if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // ✓ reject unexpected alg
200
+ return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
201
+ }
202
+ return secret, nil
203
+ })
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Command Injection & SSRF
209
+
210
+ ```go
211
+ import "os/exec"
212
+
213
+ // ✗ Command injection — user input in a shell string
214
+ exec.Command("sh", "-c", "convert "+userFile+" out.png").Run()
215
+
216
+ // ✓ Safe — pass args as a slice; no shell, no word-splitting
217
+ exec.Command("convert", userFile, "out.png").Run()
218
+ ```
219
+
220
+ ```go
221
+ // ✗ SSRF — fetching a user-supplied URL lets attackers hit internal services / metadata
222
+ http.Get(r.FormValue("url"))
223
+
224
+ // ✓ Resolve + validate the host against an allow-list and block private/link-local IPs
225
+ // before dialing (deny 169.254.169.254, 10/8, 127/8, ::1, etc.).
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Secrets & Config
231
+
232
+ ```go
233
+ // ✓ Read secrets from the environment / a secrets manager — never hardcode
234
+ dsn := os.Getenv("DATABASE_URL")
235
+ if dsn == "" {
236
+ log.Fatal("DATABASE_URL is required")
237
+ }
238
+
239
+ // ✗ Never commit credentials
240
+ const apiKey = "sk_live_8f2b..." // gitleaks/gosec will flag this
241
+ ```
242
+
243
+ Run `gitleaks` in CI and keep `.env` out of git. Use `errors.Is`/`%w` and return generic
244
+ client errors — never write `err.Error()` (which can leak DSNs, paths, SQL) into responses.
245
+
246
+ ---
247
+
248
+ ## ASVS Controls for Go Projects
249
+
250
+ | ASVS Ref | Control | Go Implementation |
251
+ |----------|---------|-------------------|
252
+ | V2.2.1 | Input validation | `go-playground/validator` on bound structs |
253
+ | V1.3.1 | Output encoding (XSS) | `html/template`; sanitise with `bluemonday` |
254
+ | V1.2.4 | No SQL injection | `database/sql` placeholders; GORM `?` params |
255
+ | V1.2.5 | No OS command injection | `exec.Command` with arg slices, no `sh -c` |
256
+ | V11.4.2 | Password storage | `bcrypt` (cost ≥ 12) or `argon2id` |
257
+ | V3.5.1 | CSRF | `gorilla/csrf` for cookie-based sessions |
258
+ | V4.1.1 | Security headers | `unrolled/secure` middleware |
259
+
260
+ ---
261
+
262
+ ## Recommended Security Tooling (2026)
263
+
264
+ | Category | Tool |
265
+ |----------|------|
266
+ | SAST | `gosec` (`securego/gosec`) |
267
+ | Vulnerable dependencies | `govulncheck` (official, stdlib-aware) |
268
+ | Secret scanning | `gitleaks` |
269
+ | Input validation | `go-playground/validator` |
270
+ | HTML sanitisation | `bluemonday` |
271
+ | Security headers | `unrolled/secure` |
272
+ | CSRF | `gorilla/csrf` |
273
+ | Password hashing | `golang.org/x/crypto/bcrypt`, `argon2` |
274
+ | CORS | `rs/cors`, `gin-contrib/cors` |
package/stacks/nextjs.md CHANGED
@@ -164,12 +164,12 @@ export async function createPost(formData: FormData) {
164
164
 
165
165
  | ASVS Ref | Control | Next.js Implementation |
166
166
  |----------|---------|----------------------|
167
- | V4.1.1 | Authentication on all endpoints | `getServerSession()` in every route handler and Server Action |
168
- | V4.2.1 | Object-level authorisation | Check `resource.userId === session.user.id` before returning/modifying |
169
- | V5.1.3 | Input validation | Zod schemas on all Server Action inputs |
170
- | V7.1.1 | Log security events | Log auth events in NextAuth callbacks |
171
- | V9.1.1 | TLS everywhere | Enforced by Vercel/host; add HSTS header |
172
- | V14.4.1 | Security headers | `securityHeaders` in next.config.js |
167
+ | V8.3.1 | Authentication on all endpoints | `getServerSession()` in every route handler and Server Action |
168
+ | V8.2.2 | Object-level authorisation | Check `resource.userId === session.user.id` before returning/modifying |
169
+ | V2.2.1 | Input validation | Zod schemas on all Server Action inputs |
170
+ | V16.2.5 | Log security events | Log auth events in NextAuth callbacks |
171
+ | V12.2.1 | TLS everywhere | Enforced by Vercel/host; add HSTS header |
172
+ | V4.1.1 | Security headers | `securityHeaders` in next.config.js |
173
173
 
174
174
  ---
175
175
 
package/stacks/rails.md CHANGED
@@ -225,12 +225,12 @@ session fixation, and many other Rails-specific issues.
225
225
 
226
226
  | ASVS Ref | Control | Rails Implementation |
227
227
  |----------|---------|---------------------|
228
- | V2.1.1 | Password complexity | Devise validates :password strength |
229
- | V2.2.1 | Account lockout | Devise :lockable |
230
- | V4.1.1 | Auth on all actions | `before_action :authenticate_user!` |
231
- | V4.2.1 | Object-level auth | Pundit policies with `authorize @resource` |
232
- | V5.3.4 | No SQL injection | ActiveRecord ORM; never string-interpolate in where() |
233
- | V14.4.5 | CSRF | `protect_from_forgery` (default) |
228
+ | V6.2.1 | Password complexity | Devise validates :password strength |
229
+ | V6.3.1 | Account lockout | Devise :lockable |
230
+ | V8.3.1 | Auth on all actions | `before_action :authenticate_user!` |
231
+ | V8.2.2 | Object-level auth | Pundit policies with `authorize @resource` |
232
+ | V1.2.4 | No SQL injection | ActiveRecord ORM; never string-interpolate in where() |
233
+ | V3.5.1 | CSRF | `protect_from_forgery` (default) |
234
234
 
235
235
  ---
236
236