agentleFS
Wade-DevCodeWade-DevCode/awesome-coding-skills-cn/skills/input-validation/SKILL.md

input-validation

Skill7 starsChanged 5 months agoMITView on GitHub

处理外部输入时使用。在边界统一校验,防脏数据与注入。

10 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use rate-limiting, observability and 1 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • harperaaSkill

    rate-limiting

    Implement rate limiting to prevent brute force attacks, spam, and resource abuse. Use this skill when you need to protect endpoints from automated attacks, prevent API abuse, limit request frequency, or control infrastructure costs. Triggers include "rate limiting", "rate limit", "brute force", "prevent spam", "API abuse", "resource exhaustion", "DoS", "withRateLimit", "too many requests", "429 error".

    Also in 3 projects that use this

  • OpenAISkill

    observability

    Vercel Observability expert guidance — Drains (logs, traces, speed insights, web analytics), Web Analytics, Speed Insights, runtime logs, custom events, OpenTelemetry integration, and monitoring dashboards. Use when instrumenting, debugging, or optimizing application performance and user experience on Vercel.

    Also in 3 projects that use this

  • GitHubSkill

    refactor

    Surgical code refactoring to improve maintainability without changing behavior. Covers extracting functions, renaming variables, breaking down god functions, improving type safety, eliminating code smells, and applying design patterns. Less drastic than repo-rebuilder; use for gradual improvements.

    Also in 3 projects that use this

The file

Files
SKILL.md

输入校验

何时用

  • 编写 API 接口、表单处理、文件上传等接收外部数据的代码时。
  • 处理来自消息队列、第三方 Webhook、数据库读回等任何外部数据源时。
  • 发现生产环境出现类型错误、字段缺失崩溃、注入攻击等问题时。
  • 做安全审查时检查系统边界的防御是否完整。

核心规则

1. 在系统边界集中校验:类型、范围、格式、必填

规则: 在 API 入口(controller/route handler)对所有外部输入做统一校验,覆盖类型是否正确、数值是否在合法范围内、字符串格式是否符合预期、必填字段是否存在;校验通过后的数据才传入业务逻辑层。

为什么: AI 生成 API 接口时最常见的模式是直接把 request.json 传进业务函数,让业务函数自己处理字段缺失或类型错误。这导致两个问题:一是 AttributeError/KeyError 在内层崩溃,堆栈信息可能暴露内部结构;二是相同的校验逻辑散落在各处,维护困难,容易漏掉。AI 还经常只校验"快乐路径",完全忽略 age=-1、quantity=99999999、email=""等边界值。

怎么做:

from pydantic import BaseModel, Field, field_validator
from typing import Literal

class CreateOrderRequest(BaseModel):
    product_id: int = Field(gt=0)                    # ✅ 类型 + 范围
    quantity: int = Field(ge=1, le=100)              # ✅ 1~100 之间
    email: str = Field(pattern=r'^[\w.+-]+@[\w-]+\.[a-z]{2,}$')  # ✅ 格式
    channel: Literal["web", "app", "api"]            # ✅ 枚举白名单

@app.post("/orders")
def create_order(body: CreateOrderRequest):          # ✅ 入口即校验
    return order_service.create(body)                # 业务层拿到的已是合法数据
  • 使用 Pydantic(Python)、Zod(TypeScript)、Joi(Node.js)等成熟校验库,不手写正则堆砌。

2. 白名单优先于黑名单;枚举/路径/文件名严格限定

规则: 对枚举值用白名单(只接受已知合法值),不用黑名单(拒绝已知危险值);文件名和路径做规范化后校验,防止路径穿越(../../../etc/passwd);用户可控的文件名只允许 [a-zA-Z0-9._-]。

为什么: AI 实现文件操作时惯用黑名单:if ".." in filename: reject。这类黑名单极容易被绕过——URL 编码 %2e%2e、双重编码 %252e%252e、Unicode 等价字符都能轻易规避。路径穿越漏洞至今仍是 OWASP Top 10 常见漏洞之一,很大程度上因为开发者(包括 AI)低估了绕过黑名单的攻击面。

怎么做:

import os
import re

ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".pdf"}
UPLOAD_BASE = "/var/uploads"

def safe_filename(user_filename: str) -> str:
    # ✅ 白名单字符集:只保留安全字符
    name = re.sub(r'[^a-zA-Z0-9._-]', '_', os.path.basename(user_filename))
    ext = os.path.splitext(name)[1].lower()

    # ✅ 扩展名白名单
    if ext not in ALLOWED_EXTENSIONS:
        raise ValueError(f"不支持的文件类型: {ext}")
    return name

def safe_path(base_dir: str, user_path: str) -> str:
    # ✅ 规范化后确认仍在 base_dir 内,防路径穿越
    full = os.path.realpath(os.path.join(base_dir, user_path))
    if not full.startswith(os.path.realpath(base_dir) + os.sep):
        raise ValueError("路径穿越攻击")
    return full

3. 校验与净化分开:先拒绝,再转义/规范化

规则: 校验阶段只做是/否判断,不合法就拒绝并返回错误;净化(escaping/sanitization)是针对通过校验的合法值,根据输出目标做的格式适配,是两个独立步骤,不能互相替代。

为什么: AI 经常把"净化"当作万能解决方案:sanitize(input) 之后就不管三七二十一地拿去用。这有两个问题:①净化函数因目标环境不同而不同(HTML、SQL、Shell、JSON 各有各的转义规则),用错了等于没转义;②净化改变了数据,可能让原本合法的业务值被意外篡改(用户名里的 < 被转成 &lt; 存进数据库,展示时又被双重转义)。校验和净化分开能让每一步的职责和错误来源都清晰。

怎么做:

from markupsafe import escape as html_escape

def process_comment(raw_text: str) -> dict:
    # 第一步:校验(是否合法)
    if not raw_text or not raw_text.strip():
        raise ValueError("评论内容不能为空")
    if len(raw_text) > 2000:
        raise ValueError("评论不得超过 2000 字")

    # 第二步:规范化(去掉首尾空白,统一换行符)
    normalized = raw_text.strip().replace("\r\n", "\n")

    # 存库:存原始净化后的文本(不 HTML 转义,数据库不是 HTML 上下文)
    saved_text = normalized

    # 返回前端:针对 HTML 上下文做转义(渲染时才转义,不提前)
    return {
        "text": saved_text,
        "html": str(html_escape(saved_text)).replace("\n", "<br>"),
    }

4. 不信任客户端校验,服务端必须再校验一遍

规则: 前端的输入校验只是用户体验优化(即时反馈),不是安全控制;后端 API 对每个接收到的参数必须独立校验,不能假设"前端已经校验过了"。

为什么: AI 生成全栈应用时,有时只在前端加 required、maxlength、pattern 等 HTML 属性,然后后端直接信任前端送来的数据。任何人用 curl、Postman 或浏览器开发者工具都能绕过前端校验,直接发送任意数据到后端。这类漏洞在 AI 代码审查中极为常见,因为 AI 看到前端"已有校验"就不再在后端重复。

怎么做:

// 前端:用户体验,即时反馈(不是安全控制)
<input type="number" min="1" max="100" required />

// 后端:安全边界,独立校验(不依赖前端传来的值)
app.post("/api/cart/add", async (req, res) => {
    const schema = z.object({
        productId: z.number().int().positive(),
        quantity: z.number().int().min(1).max(100),   // ✅ 后端独立校验
    });

    const result = schema.safeParse(req.body);
    if (!result.success) {
        return res.status(400).json({ errors: result.error.format() });
    }

    // 使用 result.data,不直接用 req.body
    await cartService.add(req.user.id, result.data.productId, result.data.quantity);
    res.json({ ok: true });
});

5. 失败给明确错误(字段+原因),但不泄露内部细节

规则: 校验失败时返回结构化错误,告知调用方哪个字段出了什么问题("field": "email", "message": "格式不合法");但不返回数据库表结构、SQL 语句、文件路径、堆栈信息等内部细节。

为什么: AI 生成的错误处理有两个极端:一是直接返回 Python 异常信息(含 SQL 语句、文件路径),帮助攻击者了解系统内部;二是统一返回 "请求失败",开发者无法调试,合法调用方也不知道如何修正请求。恰当的错误信息是安全性与可用性的平衡点。

怎么做:

# 反例:内部细节泄露
except Exception as e:
    return jsonify({"error": str(e)}), 400
    # 可能输出:UNIQUE constraint failed: users.email (table structure leaked)

# 正例:结构化错误,不暴露内部
from pydantic import ValidationError

@app.errorhandler(ValidationError)
def handle_validation_error(e: ValidationError):
    errors = [
        {"field": ".".join(str(loc) for loc in err["loc"]), "message": err["msg"]}
        for err in e.errors()
    ]
    return jsonify({"code": "VALIDATION_ERROR", "errors": errors}), 422
    # 输出:{"code": "VALIDATION_ERROR", "errors": [{"field": "email", "message": "value is not a valid email address"}]}
  • 服务端内部错误(500)统一返回 {"code": "INTERNAL_ERROR", "message": "服务暂时不可用"},详细堆栈只写进服务端日志,不下发给调用方。

正例 / 反例

反例:无校验、信任客户端、错误泄露内部细节

# 反例
@app.route("/upload", methods=["POST"])
def upload():
    filename = request.form["filename"]    # ❌ 未校验,可含路径穿越
    content = request.files["file"].read()

    # ❌ 直接拼接用户提供的文件名
    path = f"/var/uploads/{filename}"
    with open(path, "wb") as f:
        f.write(content)

    return jsonify({"path": path})         # ❌ 返回服务器内部路径

正例:白名单 + 路径校验 + 结构化错误 + 不泄露内部

# 正例
import os, re
from pathlib import Path
from flask import request, jsonify

UPLOAD_DIR = Path("/var/uploads").resolve()
ALLOWED_EXT = {".jpg", ".png", ".pdf"}
MAX_SIZE = 10 * 1024 * 1024  # 10 MB

@app.route("/upload", methods=["POST"])
@login_required
def upload():
    if "file" not in request.files:
        return jsonify({"code": "MISSING_FIELD", "field": "file"}), 422

    f = request.files["file"]
    raw_name = f.filename or ""

    # ✅ 白名单字符集清洁文件名
    safe_name = re.sub(r'[^a-zA-Z0-9._-]', '_', os.path.basename(raw_name))
    ext = Path(safe_name).suffix.lower()

    if ext not in ALLOWED_EXT:
        return jsonify({"code": "INVALID_FILE_TYPE",
                        "message": f"只支持 {', '.join(ALLOWED_EXT)}"}), 422

    content = f.read()
    if len(content) > MAX_SIZE:
        return jsonify({"code": "FILE_TOO_LARGE",
                        "message": "文件不得超过 10 MB"}), 422

    # ✅ 路径穿越防护:规范化后确认仍在 UPLOAD_DIR 内
    dest = (UPLOAD_DIR / safe_name).resolve()
    if not str(dest).startswith(str(UPLOAD_DIR) + os.sep):
        return jsonify({"code": "INVALID_PATH"}), 422

    dest.write_bytes(content)
    return jsonify({"id": safe_name}), 201   # ✅ 不返回服务器内部路径

自查清单

  • API 入口对所有外部参数做了集中校验(类型、范围、格式、必填),使用成熟校验库。
  • 枚举值使用白名单限定,文件名/路径经过白名单字符集过滤和路径穿越防护。
  • 校验(拒绝非法)和净化(转义合法值)是两个独立步骤,未将净化当作校验的替代。
  • 后端对所有参数独立校验,没有信任"前端已校验"的假设。
  • 校验失败返回结构化错误(字段 + 原因),不包含数据库信息、文件路径或堆栈。
  • 服务端内部错误(500)不向调用方下发详细堆栈,只返回通用错误码。
  • 所有外部数据源(HTTP、消息队列、文件读取、第三方 Webhook)都经过入口校验。

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.