本文へ移動
cccskills
無料GitHub で公開日本語紹介

security

ユーザー入力や外部データを扱うアプリの実装で、入力検証、認証、データ範囲、秘密情報、SQLインジェクションやXSSへの対策を確認します。

原文原文の説明を見る

Secure coding practices for agent-native apps: input validation, SQL injection, XSS, secrets, data scoping, and auth. Use when writing any action, route, or component that touches user data or external input.

BuilderIO/agent-nativeスター 7,519(GitHub 7,519 + cccskills 0)
連携ツールOpenAI

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • ユーザー入力を受ける処理を実装するとき
  • APIルートの認証と権限を確認したいとき
  • APIキーの保存方法を見直したいとき
  • SQLやHTMLの脆弱性を防ぎたいとき

日本語での紹介

できること

ユーザーデータや外部入力を扱うアクション、ルート、画面部品を実装するときに、安全な設計と確認項目を示します。入力はスキーマで検証し、SQLではユーザー入力を文字列連結せず、XSSにつながる危険なHTML挿入を避けます。ユーザー指定URLをサーバーから取得する場合は、内部ネットワークへのアクセスを防ぐ仕組みを使います。APIキーやOAuthトークンなどの秘密情報は、承認された暗号化ストアに保存し、コードやレスポンスに埋め込みません。認証の有無だけでなく、利用者ごとのデータ範囲や操作権限を確認し、必要な操作には人の承認を設ける考え方も扱います。

こんなときに便利

ユーザー入力を受け取る新しいアクションを作るとき、ユーザー所有データを読むAPIルートを追加するときに向いています。認証情報の保存方法や、SQL・HTML・URL取得に潜むリスクを実装前に確認したい場合にも使えます。複数ユーザーや組織でデータを扱うアプリの権限設計にも役立ちます。

使い方の例

  • 「このアクションの入力検証と権限設定を確認して」
  • 「このAPIルートで他のユーザーのデータが見えないか調べて」
  • 「ユーザー指定URLを安全に取得する方法に直して」

注意点

記載された仕組みは@agent-native/coreなどのフレームワーク機能を前提とします。ユーザー認証情報はリクエストの利用者や組織に紐付けて保存・解決します。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md20.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Security

Rule

Use the framework's security primitives everywhere. Never bypass them.

Absolute Secrets Rule

Never hardcode secret values or real private data. This applies to source code, docs, tests, fixtures, generated prompts, screenshots, seed data, and extension HTML just as much as production code.

Do not paste or invent real-looking API keys, bearer tokens, OAuth refresh tokens, webhook URLs, signing secrets, private Builder/internal data, or customer data into the repo. Examples must use obvious placeholders such as <OPENAI_API_KEY>, ${keys.SLACK_WEBHOOK}, sk-test-example, or example.customer@example.com. Test literals should be clearly fake and must not match real provider token formats when an example token will do.

Credential values enter the system only through approved runtime channels: deployment env vars for deploy-level secrets, the encrypted app_secrets vault or saveCredential / resolveCredential for user/org/workspace API keys, and oauth_tokens for OAuth. Code and instructions may name the credential key (OPENAI_API_KEY), but must never contain the credential value.

Input Validation

Use defineAction with a Zod schema: for every action. The framework validates input automatically and returns clear 400 errors for HTTP callers and structured error results for agent tool calls.

export default defineAction({
  schema: z.object({
    email: z.string().email(),
    role: z.enum(["admin", "member"]),
    limit: z.coerce.number().int().min(1).max(100).default(25),
  }),
  run: async (args) => { /* args is fully typed and validated */ },
});

The legacy parameters: field (plain JSON Schema) has no runtime validation — do not use it for new code.

Large Payloads

Do not accept or persist unbounded base64/file blobs through actions, SQL writes, or application_state. Route uploads through the file-upload provider and store references. This prevents database bloat, slow hot paths, and accidental secret or customer-data embedding inside binary payloads.

SQL Injection

Never concatenate user input into SQL strings. Use Drizzle ORM's query builder (always safe) or parameterized queries:

// Safe — Drizzle ORM
await db.select().from(users).where(eq(users.email, args.email));

// Safe — parameterized raw SQL
await client.execute({ sql: "SELECT * FROM users WHERE id = ?", args: [id] });

// NEVER do this
await client.execute(`SELECT * FROM users WHERE id = '${id}'`);

XSS

  • React auto-escapes JSX content — trust it.
  • Never use dangerouslySetInnerHTML, innerHTML, eval(), or document.write() with user-controlled content.
  • For rich text editing, use TipTap (framework dependency).
  • For rendering markdown, use react-markdown.

SSRF

Any server-side fetch of a user- or agent-controlled URL must go through the framework SSRF guard — a bare fetch() can be steered at cloud metadata (169.254.169.254), localhost, or internal services.

import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
// Blocks private/internal targets, re-checks the resolved IP at connect time
// (DNS rebinding), and re-validates every redirect hop.
const res = await ssrfSafeFetch(userProvidedUrl, {}, { maxRedirects: 3 });

For a pre-flight-only check (e.g. before a streaming or one-shot fetch), use isBlockedExtensionUrlWithDns(url) plus createSsrfSafeDispatcher() from the same module, and set redirect: "manual". Never let the default fetch follow redirects for an untrusted URL — a public URL can 30x into the private network.

Secrets

  • OAuth tokens go in the oauth_tokens store via saveOAuthTokens().
  • Per-user / per-org API keys go through saveCredential / resolveCredential (@agent-native/core/credentials) or the app_secrets vault. Both encrypt values at rest with AES-256-GCM (keyed by SECRETS_ENCRYPTION_KEY, falling back to BETTER_AUTH_SECRET; production refuses to start without one).
  • Never hand-roll secrets into settings, application_state, source code, or action responses sent to the client. The credential / vault APIs above are the only sanctioned stores.
  • Never commit real keys, tokens, webhook URLs, signing secrets, or private Builder/customer data in examples or fixtures. Use placeholders that cannot be mistaken for working credentials.

User Credentials Are Per-User Data — Never process.env

User credentials (API keys, third-party tokens) are per-user (or per-org) data. They MUST live in SQL, scoped per-user (u:<email>:credential:KEY) or per-org (o:<orgId>:credential:KEY). Always read with the request context:

import { resolveCredential } from "@agent-native/core/credentials";
const apiKey = await resolveCredential("OPENAI_API_KEY", { userEmail, orgId });

Values are encrypted at rest (AES-256-GCM, shared secrets/crypto.ts): saveCredential encrypts on write and resolveCredential decrypts on read, with a transparent fallback for legacy plaintext rows. The agent's raw db-query / db-exec tools also cannot read credential rows — they are excluded from the scoped settings view. To encrypt pre-existing rows in place, run pnpm action db-migrate-encrypt-credentials (idempotent, non-destructive; needs the same SECRETS_ENCRYPTION_KEY / BETTER_AUTH_SECRET as the app).

On 2026-04-29 the previous one-arg resolveCredential(key) form fell back to process.env[key] and an unscoped global settings row, so every signed-in user inherited the deployment's credentials. Two guards now block this in CI (pnpm prep):

  • scripts/guard-no-env-credentials.mjs — bans process.env.<KEY> reads in packages/core/src/credentials/, secrets/, vault/, and templates/*/server/{lib,routes/api}/credential* paths, except for an explicit allowlist of deploy-level vars (DATABASE_URL, BETTER_AUTH_SECRET, NETLIFY_*, etc.). Per-line opt-out: // guard:allow-env-credential — <reason>.
  • scripts/guard-no-unscoped-credentials.mjs — bans one-arg calls to resolveCredential / hasCredential / saveCredential / deleteCredential. Per-line opt-out: // guard:allow-unscoped-credential — <reason>.

If a deploy-level value genuinely needs an env var (CI-set token, host secret), it's not a user credential — keep it out of the credentials/ secrets/ vault/ paths and the env-credentials guard won't see it.

Guards

Two more CI guards (also wired into pnpm prep) target the 2026-04 cross-tenant leak class — request-state escaping into shared process state, and dev-mode sentinel identities used as production fallbacks.

  • scripts/guard-no-env-mutation.mjs — bans process.env.<KEY> = … (and bracket / compound forms) anywhere in production code. On serverless, every warm container handles many concurrent requests in one Node process, so process.env mutation leaks across in-flight requests (the "restore" line at the end of a handler races and never helps — most recently the Zoom webhook). Use runWithRequestContext({ userEmail, orgId, timezone }, fn) from @agent-native/core/server instead — it's AsyncLocalStorage-backed and per-request safe. Allowlisted paths: scripts/, *.spec.ts / *.test.ts, packages/core/src/dev**, templates/*/test/, anything under /cli/ or /scaffold/. Per-line opt-out: process.env.X = y // guard:allow-env-mutation — <reason>.

  • scripts/guard-no-localhost-fallback.mjs — bans the literal "local@localhost" / 'local@localhost' / `local@localhost` in production code. The bug class: getRequestUserEmail() ?? "local@localhost" silently pools every unauthenticated request into a single shared tenant, leaking credentials, tools, and application_state rows between accounts. The right behavior is to throw / 401 when there's no session. Allowlisted paths: the dev-mode auth shim (packages/core/src/server/auth.ts), packages/core/src/dev**, tests, scripts/, seed/ / seeds/, plus a few framework helpers that intentionally inspect or migrate the dev identity. SQL DDL DEFAULT 'local@localhost' and the Drizzle helper .default('local@localhost') are skipped per-line — schema column defaults are intentional dev fixtures, not the dangerous fallback pattern. Per-line opt-out: email ?? "local@localhost" // guard:allow-localhost-fallback — <reason>.

    The same guard also bans the ambient process identity as a request-scoped fallback: ?? process.env.AGENT_USER_EMAIL, || process.env.WORKSPACE_OWNER_EMAIL, and ?? getAmbientUserEmail(). Those name the identity of the deployment, not the caller, so a handler reading one authorizes whoever the deploy env names rather than whoever signed in — it fails open toward more privilege. Exempt paths are the ones with no request by construction: scripts/ (including templates/*/scripts/), src/cli/, src/scripts/, script-helpers.ts / script-entries.ts, tests, and seeds. It deliberately matches only the ?? / || fallback position, so reading the same env var to build an admin allowlist (envEmails("WORKSPACE_OWNER_EMAIL").includes(email)) is untouched. Known gaps, both requiring dataflow analysis the guard does not do: an env read aliased through a local (const fallback = process.env.AGENT_USER_EMAIL; … ?? fallback) and a destructured const { AGENT_USER_EMAIL } = process.env.

Auth

  • All actions are protected by the auth guard automatically.
  • Prefer actions for normal app data. Do not hand-write /api/* routes for CRUD, data queries, or action re-exports just to add auth; action endpoints already get auth and request context.
  • If you must create custom /api/ routes, always call getSession(event) and reject requests without a session:
import { getSession } from "@agent-native/core/server";

export default defineEventHandler(async (event) => {
  const session = await getSession(event);
  if (!session) throw createError({ statusCode: 401 });
  // ...
});
  • Never create unprotected routes that modify data.

Same-Origin Workspace Apps

Path-mounted workspace apps share the Dispatch gateway origin. Because the framework's session cookie is scoped to /, a mounted pane receives the ambient Dispatch session and can act as the signed-in user through same-origin requests. This is an intentional trusted-code model, not an isolation boundary; removing a mounted app from SSO fanout does not revoke that ambient access.

Only trusted, workspace-owner-authored code belongs on that origin. Treat each of these as a trust-boundary change that requires an explicit origin, sandbox, or capability design before shipping:

  • non-owners can create or edit mounted app code;
  • mounted apps render or execute untrusted external content or remote code;
  • apps are publicly shareable, anonymously reachable, or installable by users outside the owning workspace;
  • app source, dependencies, or deployment artifacts can be replaced by a third party without the workspace owner's authorization;
  • a mounted app can be registered across workspaces or can change its mount path/origin without an ownership check; or
  • cookie scope, proxying, iframe policy, or navigation changes make this ambient-session behavior broader than the owning workspace.

Do not describe canonical-only SSO eligibility as origin isolation. Canonical apps and explicitly registered custom origins remain eligible; path-mounted apps are deliberately excluded as SSO targets while retaining their existing same-origin session behavior.

Exception — the SSR HTML/.data catch-all is deliberately session-blind. The rule above is for routes that read or mutate user data. The SSR page render and React Router .data route are different: they serve one impersonal, public-cacheable shell to every visitor by design, and loaders on that path render no user data. Do not "fix" this by adding getSession, private, or no-store to it — that regresses the CDN cache contract for the whole site. Data scoping lives in actions and API routes; the client gates private UI after the shell loads. See the authentication skill and guard:ssr-cache-shell / ssr-handler.spec.ts.

Authorization beyond "is there a session" — authorize. The auth guard only proves someone is signed in. To restrict an operation to some teammates, set authorize on the defineAction. It wraps run, so it applies at every dispatch site (agent tool, HTTP, frontend, MCP, A2A, CLI) — unlike needsApproval, which is honored by the agent loop and MCP 2026 hosts that support elicitation, but is not an authorization boundary for every dispatch site.

import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)

export default defineAction({
  description: "Archive a client roster.",
  schema: z.object({ id: z.string() }),
  authorize: coachAccess.requireAny("coach-admin"),
  run: async (args) => {
    /* ... */
  },
});

A guard that throws denies with its own message; returning false denies generically; anything else (including undefined) allows. Guarded actions need a user identity — an unattended CLI/cron caller with no user email is denied.

authorize is not a substitute for accessFilter / assertAccess. It decides whether this caller may perform the operation at all; accessFilter / assertAccess scope which rows a permitted caller may see or touch. A restricted write action needs both. See the authentication skill for defineAppRoles and the actions skill for the full surface.

Human-in-the-Loop Approval for High-Consequence Actions

For a small set of outward-facing, hard-to-undo operations — sending an email, charging a card, deleting an account, posting publicly — auth and access control are necessary but not sufficient: you also do not want the agent to perform them autonomously. Set needsApproval on the defineAction so the agent cannot run the action without a human approving the specific call.

export default defineAction({
  description: "Send an email via Gmail.",
  schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
  needsApproval: true, // or (args, ctx) => boolean | Promise<boolean>
  run: async (args) => {
    /* ...actually send... */
  },
});

When the gate is truthy and the call is not yet approved, the agent loop emits an approval_required event and stops the turn — run() never executes. The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's stable approvalKey; only then does the action run. MCP 2026 hosts receive an input_required approval request bound to the exact caller, action, and arguments. The signed response state is backed by a durable, single-use grant, so denial, expiry, replay, missing host support, or invalid state fails closed before run(). A predicate gates conditionally (e.g. only external recipients) and fails closed — a throw is treated as "approval required".

Rules:

  • Reach for needsApproval only for genuinely high-consequence operations. The default is off, and the framework intentionally keeps approvals rare — over-gating turns the agent into a click-through wizard. The canonical (and intentionally lone) framework example is Mail's send-email.
  • needsApproval is not a substitute for accessFilter / assertAccess or for hiding sensitive operations from the model with agentTool: false / toolCallable: false. It is the layer for "a human must explicitly bless this specific outward-facing call," not for scoping data. See the actions skill for the full surface.

Custom HTTP Routes Must Apply Access Control Themselves

This is the single most-failed rule in the codebase. Auto-mounted action routes (/_agent-native/actions/...) get a request context wired up automatically. Hand-written /api/* Nitro routes do not. If your handler queries an ownable resource (any table with ...ownableColumns()), you MUST:

  1. Read the session: const session = await getSession(event).catch(() => null).
  2. Run the work inside runWithRequestContext({ userEmail: session?.email, orgId: session?.orgId }, fn) from @agent-native/core/server.
  3. Inside fn, query through one of:
    • accessFilter(table, sharesTable) in the WHERE clause for list/read-many.
    • resolveAccess("<type>", id) for read-by-id (returns null if no access — return 404, not 403, so existence isn't leaked).
    • assertAccess("<type>", id, "viewer"|"editor"|"admin") for write/delete-by-id.
// Bad — Brent's signup leaked every other user's decks because of this exact shape.
export default defineEventHandler(async () => {
  const db = getDb();
  return db.select().from(schema.decks); // no access filter!
});

// Good
import { getSession, runWithRequestContext } from "@agent-native/core/server";
import { accessFilter } from "@agent-native/core/sharing";
export default defineEventHandler(async (event) => {
  const session = await getSession(event).catch(() => null);
  return runWithRequestContext(
    { userEmail: session?.email, orgId: session?.orgId },
    async () => {
      const db = getDb();
      return db
        .select()
        .from(schema.decks)
        .where(accessFilter(schema.decks, schema.deckShares));
    },
  );
});

scripts/guard-no-unscoped-queries.mjs runs in pnpm prep and fails the build if any file in templates/*/server/, templates/*/actions/, or packages/*/src/ queries an ownable table without one of the access helpers. Last-resort opt-out is the marker comment // guard:allow-unscoped — <reason> — only use it for cases like the sharing primitives themselves or share-token-public viewer endpoints, and always include a reviewer-readable reason.

Data Scoping

In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views. This is enforced at the SQL level — the agent cannot bypass it.

The db-query / db-exec tools (and the extension SQL bridge, which shares the same path) reject schema-qualified table references like public.<table> — a qualified name resolves to the base table and would skip the temp view. Use bare table names; scoping is applied automatically.

Per-User Scoping (owner_email)

Every template table with user data must have an owner_email text column:

  1. Framework detects owner_email via schema introspection
  2. Creates temp views WHERE owner_email = <current user> before each query
  3. Auto-injects owner_email into INSERT statements

The current user is resolved from AGENT_USER_EMAIL (set automatically from the session).

Per-Org Scoping (org_id)

For multi-org apps, tables also need org_id:

  1. WHERE org_id = <current org> is added (in addition to owner_email if present)
  2. org_id is auto-injected into INSERT statements

Enable org scoping in the agent-chat plugin:

createAgentChatPlugin({
  resolveOrgId: async (event) => {
    const ctx = await getOrgContext(event);
    return ctx.orgId;
  },
});

Column Conventions

ColumnPurposeRequired
owner_emailPer-user data isolationYes, for all user-facing tables
org_idPer-org data isolationYes, for multi-org apps

Run pnpm action db-check-scoping to verify. Use --require-org for multi-org apps.

Checklist

  • New action uses defineAction with a Zod schema:
  • No SQL string concatenation with user input
  • No dangerouslySetInnerHTML with user content
  • Server-side fetches of user/agent URLs use ssrfSafeFetch, not bare fetch
  • Secrets stored via saveCredential / the vault (encrypted), never raw in settings or responses
  • No hardcoded API keys, tokens, webhook URLs, signing secrets, real credential-looking strings, private Builder/internal data, or customer data
  • New env vars in .env only, not committed
  • New user-data tables have owner_email column
  • Custom routes call getSession and reject unauthenticated requests
  • Actions only some teammates may run set authorize (in addition to any accessFilter / assertAccess row scoping)

Related Skills

  • storing-data — SQL patterns and the agent's db tools
  • actions — defineAction with Zod schema validation
  • authentication — Auth modes, sessions, and org context

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

a2a-assets

無料概要あり

複数のアプリやMCPホストからブランド画像・動画を生成、調整、書き出し、挿入する作業を支援します。ライブラリ選択や修正依頼も扱います。

  • スライド用のブランド画像を作りたいとき
  • 既存素材をフィードバックに合わせて直したいとき
  • 動画素材を生成して書き出したいとき
BuilderIO/agent-native7,5192026年10月26日 更新

account-health

無料概要あり

特定の顧客企業について、契約と製品利用の数値を照らし合わせ、根拠や不足情報を明記した健全性レポートをまとめます。

  • 顧客の健全性を確認したいとき
  • 契約席数と実利用を比べたいとき
  • 更新前に利用状況を整理したいとき
BuilderIO/agent-native7,5192026年10月26日 更新

actions

無料概要あり

アプリの操作をエージェントと画面の両方から呼び出せるアクションとして実装し、API連携や画面のデータ取得を一貫した形で追加するスキルです。

  • 新しいアプリ操作を追加したいとき
  • 外部APIと連携したいとき
  • 画面からデータを取得したいとき
BuilderIO/agent-native7,5192026年10月26日 更新

adding-a-feature

無料概要あり

新機能をUI・エージェント用アクション・説明資料・画面状態の4領域でそろえ、利用者とエージェントのどちらからも一貫して使えるようにするチェックリストです。

  • 新機能追加の作業範囲を整理したいとき
  • UIとエージェント操作をそろえたいとき
  • 画面状態の同期漏れを防ぎたいとき
BuilderIO/agent-native7,5192026年10月26日 更新

adhoc-analysis

無料概要あり

複数のデータソースを照合して、傾向や指標、根拠をまとめた分析結果を作成します。必要に応じて、再実行できるダッシュボードとして保存できます。

  • 商談と通話記録を照合したいとき
  • 顧客の利用状況を横断して調べたいとき
  • 繰り返し更新できる分析を保存したいとき
BuilderIO/agent-native7,5192026年10月26日 更新

admin-surfaces

無料概要あり

Analyticsの管理画面で、機能フラグの状態確認と切り替え、ダッシュボード利用状況の調査、接続先アプリのデータベース管理を行います。

  • アプリごとの機能フラグを確認・変更したいとき
  • ダッシュボードの所有者や利用状況を調べたいとき
  • 接続済みアプリのテーブルを管理したいとき
BuilderIO/agent-native7,5192026年10月26日 更新

BuilderIO のスキルをすべて見る

このスキルの問題を報告する