logo
Zhanxin
Published on

如何使用 Zod 库实现文件输入验证

Authors

本文共有 1730 字 · 预计阅读时长≈ 9 min

在 Web 开发中,表单验证对于应用的安全性和完整性至关重要。表单作为用户与应用的交互窗口,接收用户输入,也成为恶意用户潜在攻击的入口。攻击者可以通过表单输入恶意脚本或文件,从而威胁应用的完整性并危及用户隐私数据。

尽管文本输入通常是脚本注入的首选目标,但文件输入也存在安全风险。恶意用户可能通过文件上传隐蔽地嵌入恶意脚本。为防范此类风险,有效的表单验证至关重要。

Zod 是一个适用于 JavaScript 和 TypeScript 的数据验证库,提供强类型支持,简化数据验证并增强输入安全性。本文将深入讲解如何使用 Zod 对文件输入进行验证,以保障应用安全。

Zod 简介

Zod 是一个 TypeScript 优先的架构验证库,能够模拟 TypeScript 的强类型安全性,确保运行时的数据结构符合预期。通过 import { z } from 'zod' 引入后,即可使用 z 对象定义各种数据类型,例如字符串、数值、布尔值、日期等。示例代码如下:

import { z } from "zod";

// Basic primitive schemas
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const undefinedSchema = z.undefined();
const nullSchema = z.null();

// Object Schema
const userSchema = z.object({
  name: z.string(),
  age: z.number().int(), //built-in method: Only allows integers
  email: z.string().email(), //built-in method: Validates email format
});

// Array schema
const stringArraySchema = z.array(z.string());

// Optional and Nullable types
const userSchema = z.object({
  name: z.string(),
  age: z.number().int().optional(), // age is optional
  email: z.string().email().nullable(), // email can be null
});

Zod 的语法简洁明了,与 TypeScript 完美结合,是实现数据验证的理想选择。

项目初始化

在本示例中,您需要安装 React 和 Zod。通过 Vite 创建 React TypeScript 项目,并安装 Zod:

// create react app with vite
npm create vite@latest file-input-validation --template react-ts
cd file-input-validation
npm install

// install zod
npm install zod

设置完成后,启动本地服务器

npm run dev

现在,应用程序已成功运行,删除 App.css 文件,清除 App.tsx 和 index.css 文件,并将前者替换为下面的代码块。

// App.tsx

/* eslint-disable @typescript-eslint/no-explicit-any */
import { DOCUMENT_SCHEMA, IMAGE_SCHEMA } from "./utils/schema";
import { useState, useEffect } from "react";
interface ErrorType {
  img_upload?: string;
  doc_upload?: string;
}
function App() {
  const [docFile, setDocFile] = useState<File | undefined>();
  const [imgFile, setImgFile] = useState<File | undefined>();
  const [imgUrl, setImgUrl] = useState("");
  const [error, setError] = useState<ErrorType>({});
  useEffect(() => {
    if (imgFile) {
      const url = URL.createObjectURL(imgFile);
      setImgUrl(url);
      return () => URL.revokeObjectURL(url);
    }
  }, [imgFile]);

  const handleDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {

  };
  const handleImgChange = (e: React.ChangeEvent<HTMLInputElement>) => {

  };
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

  };
  return (
    <div className="app-container">
      <h1>File Input Validation with Zod</h1>
      <div className="form-container">
        <form className="form" onSubmit={handleSubmit}>
          <div className="formfield">
            <label htmlFor="doc-input">
              <p>Document Input</p>
              <div className="doc-label">
                {docFile?.name ? (
                  <p>{docFile?.name}</p>
                ) : (
                  <p>
                    <span>Browse</span> to upload document here{" "}
                  </p>
                )}
                <p className="size">(5MB Max)</p>
              </div>
            </label>
            <input
              id="doc-input"
              name="doc_upload"
              type="file"
              onChange={handleDocChange}
              accept="application/*"
            />
            {error.doc_upload && <p className="error">{error.doc_upload}</p>}
          </div>
          <div className="formfield">
            <label htmlFor="img-input">
              <p>Image Input</p>
              <div className="image-label">
                {imgUrl ? (
                  <img src={imgUrl} alt="img-input" />
                ) : (
                  <div>
                    <p>
                      <span>Browse</span> to upload image here{" "}
                    </p>
                    <p className="size">(5MB Max)</p>
                  </div>
                )}
              </div>
            </label>
            <input
              id="img-input"
              name="img_upload"
              type="file"
              accept="image/*"
              onChange={handleImgChange}
            />
            {error.img_upload && <p className="error">{error.img_upload}</p>}
          </div>
          <button
            type="submit"
            disabled={
              !!error.doc_upload || !!error.img_upload || !docFile || !imgFile
            }
          >
            Submit
          </button>
        </form>
      </div>
    </div>
  );
}
export default App;

文件类型验证

HTML 文件输入元素可接收多种文件类型,例如图像、文档等。通过 accept 属性,可限制允许上传的文件类型,防止用户上传潜在危险文件(如 .exe 文件)。在前端,使用 Zod 可以进一步强化验证,确保文件的类型和大小符合要求。

utils/schema.ts 文件中,我们可以定义文件的验证模式。以下示例展示了如何使用 Zod 验证文档和图像文件类型:

// utils/schema
import { z } from "zod";

// Document Schema
export const DOCUMENT_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "application/pdf",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ].includes(file.type),
    { message: "Invalid document file type" }
  );

// Image Schema
export const IMAGE_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/svg+xml",
        "image/gif",
      ].includes(file.type),
    { message: "Invalid image file type" }
  );

文件大小验证

除了文件类型验证之外,您还可以扩充 schema 验证文件大小。

import { z } from "zod";
const fileSizeLimit = 5 * 1024 * 1024; // 5MB

// Document Schema
export const DOCUMENT_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "application/pdf",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ].includes(file.type),
    { message: "Invalid document file type" }
  )
  .refine((file) => file.size <= fileSizeLimit, {
    message: "File size should not exceed 5MB",
  });

// Image Schema
export const IMAGE_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/svg+xml",
        "image/gif",
      ].includes(file.type),
    { message: "Invalid image file type" }
  )
  .refine((file) => file.size <= fileSizeLimit, {
    message: "File size should not exceed 5MB",
  });

前端验证集成

在 App.tsx 文件中,我们可以引入 Zod Schema 并编写文件验证函数:


import { DOCUMENT_SCHEMA, IMAGE_SCHEMA } from "./utils/schema";

const validateFile = (file: File, schema: any, field: keyof ErrorType) => {
    const result = schema.safeParse(file);
    if (!result.success) {
      setError((prevError) => ({
        ...prevError,
        [field]: result.error.errors[0].message,
      }));
      return false;
    } else {
      setError((prevError) => ({
        ...prevError,
        [field]: undefined,
      }));
      return true;
    }
  };
  const handleDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const isValid = validateFile(file, DOCUMENT_SCHEMA, "doc_upload");
      if (isValid) setDocFile(file);
    }
  };
  const handleImgChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const isValid = validateFile(file, IMAGE_SCHEMA, "img_upload");
      if (isValid) setImgFile(file);
    }
  };

validateFile 函数使用 Zod 的 safeParse 方法验证文件,返回结果并在错误时显示自定义消息。

处理多个文件

通过将 multiple 属性添加到您的 input 元素,您可以一次选择并上传多个文件。使用 Zod,您可以为 FileList 编写验证模式,这与对 File 执行相同操作一样。

const fileSizeLimit = 5 * 1024 * 1024; // 5MB

export const fileUploadSchema = z.object({
  files: z
    .instanceof(FileList)
    .refine((list) => list.length > 0, "No files selected")
    .refine((list) => list.length <= 5, "Maximum 5 files allowed")
    .transform((list) => Array.from(list))
    .refine(
      (files) => {
        const allowedTypes: { [key: string]: boolean } = {
          "image/jpeg": true,
          "image/png": true,
          "application/pdf": true,
          "application/msword": true,
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
            true,
        };
        return files.every((file) => allowedTypes[file.type]);
      },
      { message: "Invalid file type. Allowed types: JPG, PNG, PDF, DOC, DOCX" }
    )
    .refine(
      (files) => {
        return files.every((file) => file.size <= fileSizeLimit);
      },
      {
        message: "File size should not exceed 5MB",
      }
    ),
});

将其与应用程序的表单输入元素集成

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const selectedFiles = e.target.files;
  if (!selectedFiles) return;
  const result = fileUploadSchema.safeParse({ files: selectedFiles });
  if (result.success) {
    setFiles(result.data.files);
    setError(null);
  } else {
    setError(result.error.errors[0].message);
    setFiles([]);
  }
};

文件上传安全建议

通过验证文件类型和大小,能减少部分安全风险,但仍需注意以下安全措施:

  • 验证 MIME 类型和扩展名:确保文件的 MIME 类型与扩展名一致,以防止恶意文件伪装。
  • 清理文件内容:对于可能含脚本的文件(如 SVG),可以在服务器端进行清理,删除潜在的恶意代码。
  • 拒绝可疑文件:不明确或不匹配 MIME 类型和扩展名的文件,应被拒绝上传。
  • 此外,文件上传可能遇到空文件、编码问题等边缘情况。合理处理这些问题,能有效提高应用的用户体验与安全性。

总结

Zod 使得文件输入验证更简单、直观,并为应用提供了额外的安全层。结合 TypeScript 使用时,Zod 带来了更强的类型安全保障。掌握表单验证和文件上传的最佳实践,不仅能提高应用安全性,还能让用户体验更加友好。

参考资料

Zod