# Practical secure-coding guide [2026 edition]: become an engineer who 'builds safely' with NIST SSDF and OWASP ASVS

> A complete guide to practicing secure coding by 'mechanism,' not 'willpower.' With NIST's official framework SSDF (SP 800-218) and OWASP ASVS 5.0 as a map, it explains validation at the trust boundary, server-enforced authorization, output encoding, secret management, and dependency measures with real code, and finally shows how to auto-enforce these in CI.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: セキュリティ, セキュアコーディング, NIST SSDF, OWASP ASVS, DevSecOps
- URL: https://tomodahinata.com/en/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide
- Category: Security engineering & career
- Pillar guide: https://tomodahinata.com/en/blog/security-engineer-how-to-become-roadmap-skills-certification-guide

## Key points

- Secure coding is realized not by individual carefulness but by a 'mechanism' along official frameworks. NIST's SSDF (SP 800-218 v1.1) systematizes safe development into 4 practice groups (PO/PS/PW/RV), and OWASP ASVS 5.0 systematizes verification requirements into 17 chapters, ~350 items, and 3 levels.
- The most important principle is 'validate at the trust boundary.' Client, API response, file, environment variable — every value coming from outside the system is schema-validated at the boundary before entering the inside. Even in a type-safe language, runtime validation at the boundary can't be omitted.
- Enforce authorization not in a UI if-statement but on the server/DB. Bake the owner condition into the query, and if not found, hide existence and fall back to 404. Cut injection in principle with parameterization and XSS with output encoding.
- Make defense the default. Security headers/CSP, a typed boundary for secrets, least privilege, safe error handling — fall back to a structure that's 'safe even if you slip,' not one that 'becomes dangerous if you slip.'
- Finally, auto-enforce in CI. Bake SSDF's practices (dependency scan, SAST, secret scan) into a GitHub Actions quality gate and dangerous code stops before entering main, without depending on a human review gap.

---

Teams where secure coding doesn't go well have something in common. **They depend on human carefulness — "be careful," "notice in review."** Carefulness tires. It fades before a deadline. Newcomers don't have it. So, **turning safety from "individual effort" into a "mechanism"** — that's the theme of this article.

Fortunately, there's no need to reinvent the wheel. **NIST and OWASP officially systematize how to build safely.** This article, with these two primary sources as a map, shows the principles of secure coding in **real code you can copy and use**, and finally runs it through to **auto-enforcing it in CI.**

> **The relationship of this article and its sister article:** "what role to aim for in the first place" is in [how to become a security engineer [complete roadmap]](/blog/security-engineer-how-to-become-roadmap-skills-certification-guide). This article digs into its core skill, "the technique of building safely." The code examples are TypeScript/Next.js-centric, but the principles are language-independent. **Concrete vulnerability detection/fixing on a specific stack (Next.js × Supabase)** is linked to individual articles in various places.

---

## 0. Two official frameworks — SSDF and ASVS

First, grasp the two maps we'll reference many times.

### NIST SSDF (Secure Software Development Framework / SP 800-218)

[SSDF (SP 800-218 v1.1)](https://csrc.nist.gov/projects/ssdf) is a development-process framework that **organizes the "how to build" of safe software into 4 practice groups** (v1.2 was published as a draft in December 2025 and is in public comment).

| Practice group | What it does |
|---|---|
| **PO (Prepare the Organization)** | preparation to build safely as an organization (policies, roles, tooling) |
| **PS (Protect the Software)** | protect the software itself and the code (tamper prevention, integrity, secret protection) |
| **PW (Produce Well-Secured Software)** | produce well-secured software (design, secure coding, review, test) |
| **RV (Respond to Vulnerabilities)** | respond to vulnerabilities (discovery, fixing, recurrence prevention) |

This article's §1–§5 are mainly **PW** (write safely), §6 is the automation of **PS/PW**, and §7 is the entrance to **RV.**

### OWASP ASVS 5.0 (Application Security Verification Standard)

[ASVS 5.0.0](https://owasp.org/www-project-application-security-verification-standard/) (published May 2025, dedicated site [asvs.dev](https://asvs.dev/)) is **a checklist of "requirements a safe app should satisfy."** It organizes about 350 verification requirements into 17 chapters, and you can choose "how far to protect" with **3 levels.**

- **Level 1:** minimum (the starting point for many apps)
- **Level 2:** the recommended level for most apps handling sensitive data
- **Level 3:** apps needing the highest level, like medical and financial

> SSDF is "how to build (process)," ASVS is "what to satisfy (requirements)." **Develop with SSDF's process and verify with ASVS's requirements** — the two are complementary.

---

## 1. The big principle: validate at the trust boundary (ASVS V1/V2)

The starting point of all secure coding is a single question. **"Was this value born inside the trustworthy, or did it come from outside the untrustworthy?"** A value from outside (user input, API response, file, environment variable) is **schema-validated at the boundary** before entering the inside.

Even in a type-safe language like TypeScript, this can't be omitted. **The type is just a compile-time promise, and there's no guarantee that data coming from outside at runtime matches the type.** At the boundary, with a runtime validator like [Zod](https://zod.dev/), "validate, and only then get the type."

```ts
// 境界バリデーション：外から来たJSONを、信じる前に検証する。
import { z } from "zod";

// ① 受け入れる形を、ホワイトリストとして厳密に宣言する。
const CreateUserSchema = z.object({
  email: z.string().email().max(254),
  displayName: z.string().min(1).max(50),
  age: z.number().int().min(0).max(150),
}).strict(); // ← strict(): 想定外のキーを拒否（マスアサインメント対策）

export async function POST(req: Request): Promise<Response> {
  const json: unknown = await req.json(); // ② 外から来た値は常に unknown 型で受ける

  const parsed = CreateUserSchema.safeParse(json); // ③ 検証してから内側へ
  if (!parsed.success) {
    // ④ エラー詳細は最小限に。内部構造を攻撃者に教えない。
    return Response.json({ error: "Invalid input" }, { status: 400 });
  }

  // ⑤ ここから先、parsed.data は「検証済みの型」として安全に使える。
  const user = await createUser(parsed.data);
  return Response.json({ id: user.id }, { status: 201 });
}
```

The point is `.strict()`. By **rejecting** keys not in the schema, you can structurally cut the **mass-assignment attack** that smuggles in an unexpected field like `isAdmin: true`. "Expand the incoming value as-is and save it" is the most dangerous antipattern.

> The idea of not ending type safety as a "policy" but enforcing it as runtime validation at the boundary is systematized in [the discipline of TypeScript type safety](/blog/typescript-type-safety-discipline-zod-nevererror-no-any).

---

## 2. Enforce authorization on the server/DB (ASVS V4, OWASP Top 10 A01)

In [OWASP Top 10:2025](https://owasp.org/Top10/2025/), **#1** is "Broken Access Control." The most common and the most damaging. There are 2 principles.

1. **Separate authentication (who you are) and authorization (whether you have permission), and always do both.**
2. **Don't put authorization in the client (UI).** Hiding a button is for convenience, not defense. Attackers hit the API directly.

```ts
// ❌ 危険：URLのIDを信じている。他人のIDに変えれば他人のデータが読める（IDOR）。
const doc = await db.document.findUnique({ where: { id: params.id } });

// ✅ 安全：所有者条件をクエリに焼き込む。他人のものは「存在しない」ことにする。
const doc = await db.document.findFirst({
  where: { id: params.id, ownerId: session.userId },
});
if (!doc) return new Response("Not Found", { status: 404 }); // 403ではなく404で存在を隠す
```

"Baking the owner condition into the query" is the crux. Not **"fetch, then confirm the owner"** but **"make a query that can only fetch your own."** The latter falls back to the safe side even if you forget to write the confirmation.

**Row-level security (RLS)**, which leans authorization onto the database itself, is even stronger. Even with an app bug, the DB becomes the last fort. For concrete design, the [Supabase RLS production design](/blog/supabase-rls-production-multi-tenancy-patterns); for detection, the [broken-authorization / IDOR detection guide](/blog/nextjs-supabase-idor-broken-authorization-rls-detection-guide).

---

## 3. Injection and output encoding (ASVS V5, Top 10 A05)

Injection (SQLi, command injection, XSS) is an attack that **makes "data" be interpreted as "code."** The defense principle is universal — **don't mix data and code.**

### SQL injection: always parameterize

```ts
// ❌ 危険：文字列連結。' OR '1'='1 のような入力でクエリ構造が壊れる。
const rows = await db.query(`SELECT * FROM users WHERE email = '${email}'`);

// ✅ 安全：パラメータ化クエリ。値は常に「データ」として扱われ、コードにならない。
const rows = await db.query("SELECT * FROM users WHERE email = $1", [email]);
```

Use an ORM (Prisma / Drizzle, etc.) and parameterization is usually automatic. Be careful only when writing raw SQL or RPC — for details, [SQL-injection measures](/blog/supabase-postgres-sql-injection-rpc-prevention-guide).

### XSS: encode at output time

XSS is an attack where "user input is executed as HTML." **React escapes values inside JSX by default**, so `{userInput}` is safe. The danger is only when you remove that safety device yourself.

```tsx
// ❌ 危険：dangerouslySetInnerHTML に未検証のHTMLを渡す＝XSSの入口。
<div dangerouslySetInnerHTML={{ __html: userProvidedHtml }} />

// ✅ 安全：どうしてもHTMLを描画するなら、信頼できるサニタイザを通す。
import DOMPurify from "isomorphic-dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userProvidedHtml) }} />

// ✅ 最も安全：そもそもHTMLとして描画せず、テキストとして渡す（Reactが自動エスケープ）。
<div>{userProvidedText}</div>
```

The criterion is simple. **Doubt "yourself about to write `dangerouslySetInnerHTML`."** For details, [XSS / DOM-based XSS measures](/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide).

---

## 4. Defense by default: security headers and CSP (ASVS V13)

A good security engineer builds **a structure that's "safe even if you slip," not one that "becomes dangerous if you slip."** The representative is defense-in-depth with HTTP response headers. With one middleware, you can lay a defense floor across the whole app.

```ts
// middleware.ts — アプリ全体に「安全な既定値」を敷く（Next.js）。
import { NextResponse, type NextRequest } from "next/server";

export function middleware(_req: NextRequest): NextResponse {
  const res = NextResponse.next();

  // クリックジャッキング防止：フレーム埋め込みを禁止。
  res.headers.set("X-Frame-Options", "DENY");
  // MIMEスニッフィング防止：Content-Typeを尊重させる。
  res.headers.set("X-Content-Type-Options", "nosniff");
  // 常にHTTPSを強制（1年・サブドメイン込み）。
  res.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
  // リファラからのパス・クエリ漏えいを抑える。
  res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  // 不要なブラウザ機能を既定で無効化（最小権限）。
  res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
  // CSP：許可したオリジン以外のスクリプト実行を禁止（XSSの被害を大幅に軽減）。
  res.headers.set(
    "Content-Security-Policy",
    "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
  );

  return res;
}

export const config = { matcher: "/:path*" }; // 全ルートに適用
```

CSP (Content-Security-Policy) becomes a second wall that **"doesn't run scripts from unpermitted places"** even if XSS slips through. The production operation of a strict CSP using a nonce is in [the implementation of security headers / CSP](/blog/nextjs-security-headers-csp-nonce-middleware-guide).

---

## 5. Secrets and dependencies (SSDF PS, Top 10 A02/A03)

### Secrets: don't write in code, handle at a typed boundary

API keys, DB credentials, and signing keys must **never be written in code or the repository.** Inject them via environment variables, and what's more robust is to **separate "server-only secrets" and "public values that may go to the client" by type.**

```ts
// env.ts — 環境変数も「外から来る値」。起動時に境界で検証する。
import { z } from "zod";

// サーバー専用（クライアントへ絶対に漏らさない）。
const ServerEnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
});

// クライアントへ露出してよい公開値だけを別スキーマに分離する。
const PublicEnvSchema = z.object({
  NEXT_PUBLIC_APP_URL: z.string().url(),
});

// 起動時に検証：設定ミスは「本番で漏れる」前に「起動で落として」気づく。
export const serverEnv = ServerEnvSchema.parse(process.env);
export const publicEnv = PublicEnvSchema.parse(process.env);
```

"A secret key prefixed with `NEXT_PUBLIC_` gets baked into the bundle and leaks" is a frequent accident. Separate by type and you can make it "hard" to mistakenly design a secret on the public side (→ [preventing secret leaks from environment variables](/blog/nextjs-env-secret-leak-prevention-public-vars-guide)).

### Dependencies: code you didn't write yourself is also "your responsibility"

In a modern app, most of the code is dependency packages. If there's a known vulnerability there, it becomes your app's vulnerability (Top 10 A03 "Software Supply Chain Failures"). A mechanism to **automate dependency vulnerability scanning (SCA)** and continuously apply updates is essential. On GitHub, [Dependabot](/blog/dependabot-production-guide) is the standard answer.

---

## 6. Bake SSDF into CI — don't let dangerous code into main

The final finishing is to **auto-enforce the principles so far, without depending on human review.** Implement SSDF's PW (produce well-secured software) and PS (protect the software) as a GitHub Actions quality gate. **It runs automatically per PR and blocks the merge if there's even one danger.**

```yaml
# .github/workflows/security-gate.yml
# SSDFの実践（依存スキャン・SAST・秘密情報スキャン）をPRの必須ゲートにする。
name: security-gate

on:
  pull_request:
    branches: [main]

permissions:
  contents: read          # 最小権限：このジョブが必要なのは読み取りだけ
  security-events: write  # SAST結果(SARIF)をSecurityタブへ送るためだけに付与

jobs:
  secure-coding-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # ① 依存の脆弱性スキャン（SCA / SSDF: PW.4, RV）。既知CVEを持つ依存で落とす。
      - name: Audit dependencies
        run: npm audit --audit-level=high

      # ② 静的解析（SAST / SSDF: PW.7, PW.8）。インジェクション・認可漏れ等のパターンを検出。
      - name: Static analysis (Semgrep)
        uses: semgrep/semgrep-action@v1
        with:
          config: p/owasp-top-ten   # OWASP Top 10 のルールセット

      # ③ 秘密情報スキャン（SSDF: PS.3）。鍵やトークンの混入をマージ前に止める。
      - name: Secret scanning (Gitleaks)
        uses: gitleaks/gitleaks-action@v2

      # ④ 型・テスト（壊れた契約と退行を止める）。
      - name: Type-check and test
        run: |
          npm ci
          npm run type-check
          npm test
```

With this gate in, security changes from "a special process you do all at once right before release" to **"a daily constraint that auto-applies per PR."** This is the heart of DevSecOps and the figure SSDF aims for.

> **An honest line**: where this auto-gate is strong is the **mechanically-detectable "horizontal controls"** — dependency vulnerabilities, known dangerous patterns, and secret intrusion. On the other hand, **"vertical risks" like "is this authorization logic correct as a business rule" and "is there a gap in this tenant separation" can't be judged by tools.** That's the domain of human design review/audit. The boundary of what can and can't be automated is honestly summarized in [what a security audit looks at](/blog/nextjs-supabase-security-audit-scope-when-needed-guide). The CI-gate implementation (through SARIF integration) for a Next.js × Supabase configuration is in [security CI / SARIF](/blog/nextjs-supabase-security-ci-sarif-github-actions-guide).

---

## 7. Use ASVS as a checklist

Finally, the yardstick to measure whether the implementation so far is "enough" is [ASVS 5.0](https://asvs.dev/). The usage is simple.

1. **Choose the level from the app's sensitivity.** For a general SaaS, level 2 is the guide.
2. **Make the relevant chapter's requirements a checklist for PRs and releases.** Confirm "is it satisfied" per chapter: authentication (V6), session management, access control (V4), input validation (V2), cryptography, logging…
3. **Record items you can't satisfy as risks.** You don't need everything perfect. **"Grasping what you're protecting and what you're not"** is decisively more important than not grasping it.

The correct stance is to use ASVS not as a command document of "do everything" but as **"a map that visualizes gaps."**

---

## 8. Frequently asked questions (FAQ)

**Q. Do these principles apply even if the language differs?**
A. Yes. The code examples are TypeScript, but **validation at the trust boundary, server-enforced authorization, parameterization, output encoding, and not writing secrets in code** are universal principles common to Go, Python, Java, and Ruby. Both SSDF and ASVS are language-independent.

**Q. If I use an ORM, do I not need to worry about SQL injection?**
A. Ordinary queries are auto-parameterized so they're safe. But be careful with spots that go outside the ORM, like **raw SQL, stored procedures, RPC, and dynamic ORDER BY.** Don't stop thinking with "I'm using an ORM so it's safe"; for spots where you write raw SQL, always confirm parameterization.

**Q. SSDF or ASVS — which to learn first?**
A. If you're an implementer, entering **from ASVS's requirements (what to satisfy)** is concrete and easy to grasp. If you're in a position to design teams and processes, **SSDF (how to build)** works. Going back and forth between both is ideal.

**Q. Doesn't a CI gate slow development?**
A. In the short term, PRs stop a bit. But because **a vulnerability's fix cost skyrockets the later it's found**, stopping it at PR time is overwhelmingly cheaper. A realistic staged introduction is to first introduce the gate as "warning only" and raise it to "block" once the team is used to it.

**Q. I don't have time to do everything. What's the priority?**
A. The OWASP Top 10 order, i.e., **① access control (authorization) ② security configuration (headers/secrets) ③ injection** first. In order of large damage and high frequency. Don't aim for perfection; start steadily from here.

---

## 9. Conclusion

Secure coding is neither talent nor willpower but **a "mechanism" along official frameworks.**

- Use **SSDF (how to build) and ASVS (what to satisfy)** as a map. Don't reinvent the wheel.
- **Validate at the trust boundary.** Schema-validate every value from outside before the inside (cut mass assignment with `.strict()` too).
- **Enforce authorization on the server/DB.** Bake the owner condition into the query, and hide existence with 404 if not found.
- **Don't mix data and code.** Cut injection in principle with parameterization and XSS with output encoding.
- **Defense by default.** Security headers/CSP, a typed boundary for secrets, least privilege.
- **Auto-enforce in CI.** Make dependency scan, SAST, and secret scan a quality gate and don't depend on human review gaps.

And remember. **What automation can protect is only "horizontal controls."** "Vertical risks" like authorization and tenant separation rooted in business rules need **human design judgment.** That's exactly why — sweep the horizontal with a mechanism, and concentrate human time on the vertical — that's the practical wisdom of a security engineer to get the maximum safety with limited resources.

---

### References (official primary sources)

- Frameworks: [NIST SSDF (SP 800-218)](https://csrc.nist.gov/projects/ssdf) / [OWASP ASVS 5.0](https://owasp.org/www-project-application-security-verification-standard/) ([asvs.dev](https://asvs.dev/))
- Standards: [OWASP Top 10:2025](https://owasp.org/Top10/2025/) / [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
- Tools: [Semgrep](https://semgrep.dev/) / [Gitleaks](https://github.com/gitleaks/gitleaks) / [Zod](https://zod.dev/)
- Related: [how to become a security engineer (roadmap)](/blog/security-engineer-how-to-become-roadmap-skills-certification-guide)
