@letar/forms

Security Patterns

Honeypot, rate limiting, and secure file uploads for form protection

Overview

Forms are the primary attack surface for bots, spam, and malicious uploads. @letar/forms provides three built-in security mechanisms that require minimal configuration.

Full example

The complete sandbox example, read directly from the form-develop-app source at build time.

apps/form-develop-app/src/app/security-demo/page.tsx
'use client'

import { Code, Heading, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { z } from 'zod/v4'
import { DemoPageLayout } from '../_components'

const ContactSchema = z
  .object({
    name: z
      .string()
      .min(1, 'Required')
      .meta({ ui: { title: 'Name' } }),
    email: z
      .string()
      .email()
      .meta({ ui: { title: 'Email' } }),
    message: z
      .string()
      .min(10, 'Min 10 characters')
      .meta({ ui: { title: 'Message' } }),
  })
  .strip()

export default function SecurityDemoPage() {
  const [result, setResult] = useState<string | null>(null)
  const [fileResult, setFileResult] = useState<string | null>(null)

  return (
    <DemoPageLayout title="Security Patterns" description="Honeypot, Rate Limiting, Secure File Upload">
      {/* Honeypot */}
      <VStack gap={4} align="stretch">
        <Heading size="lg">1. Honeypot — ловушка для ботов</Heading>
        <Text color="fg.muted">Скрытое поле невидимо для людей. Боты заполняют все поля — submit блокируется.</Text>

        <Form
          initialValue={{ name: '', email: '', message: '' }}
          schema={ContactSchema}
          honeypot={true}
          onSubmit={(data) => setResult(JSON.stringify(data, null, 2))}
        >
          <Form.Field.String name="name" />
          <Form.Field.String name="email" />
          <Form.Field.Textarea name="message" />
          <Form.Button.Submit>Отправить</Form.Button.Submit>
        </Form>

        {result && <Code whiteSpace="pre">{result}</Code>}
      </VStack>

      {/* Rate Limiting */}
      <VStack gap={4} align="stretch" mt={8}>
        <Heading size="lg">2. Rate Limiting — ограничение попыток</Heading>
        <Text color="fg.muted">Максимум 3 попытки submit за 60 секунд. После — обратный отсчёт.</Text>

        <Form
          initialValue={{ name: '', email: '', message: '' }}
          schema={ContactSchema}
          rateLimit={{ maxSubmits: 3, windowMs: 60000 }}
          onSubmit={(data) => setResult(JSON.stringify(data, null, 2))}
        >
          <Form.Field.String name="name" />
          <Form.Field.String name="email" />
          <Form.Field.Textarea name="message" />
          <Form.Button.Submit>Отправить (макс. 3 раза/мин)</Form.Button.Submit>
        </Form>
      </VStack>

      {/* Secure File Upload */}
      <VStack gap={4} align="stretch" mt={8}>
        <Heading size="lg">3. Secure File Upload</Heading>
        <Text color="fg.muted">Проверка MIME по magic bytes, удаление EXIF, переименование в UUID.</Text>

        <Form
          initialValue={{ document: [] }}
          onSubmit={(data) => {
            const files = data.document as File[]
            setFileResult(files.map((f: File) => `${f.name} (${f.type}, ${f.size} bytes)`).join(', '))
          }}
        >
          <Form.Field.FileUpload
            name="document"
            label="Загрузите документ"
            variant="dropzone"
            accept={['image/jpeg', 'image/png', 'application/pdf']}
            security={{
              maxSize: '10MB',
              allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
              stripMetadata: true,
              renameFile: true,
            }}
          />
          <Form.Button.Submit>Загрузить</Form.Button.Submit>
        </Form>

        {fileResult && <Code whiteSpace="pre">{fileResult}</Code>}
      </VStack>
    </DemoPageLayout>
  )
}

Honeypot — Bot Trap

A hidden field invisible to humans. Bots fill all fields — if this field has a value, submit is silently blocked.

<Form honeypot={true} initialValue={data} onSubmit={handleSubmit}>
  <Form.Field.String name="email" />
  <Form.Field.Textarea name="message" />
  <Form.Button.Submit>Send</Form.Button.Submit>
</Form>

How it works:

  • Renders a hidden <input> with display:none, aria-hidden, tabIndex=-1
  • Field name is randomized per render (useId + random suffix)
  • If the field contains any value on submit, the form silently does nothing
  • Zero UX impact for real users

Rate Limiting

Client-side submit throttling with countdown timer. Persists in sessionStorage.

<Form rateLimit={{ maxSubmits: 3, windowMs: 60000 }} initialValue={data} onSubmit={handleSubmit}>
  <Form.Field.String name="email" />
  <Form.Button.Submit>Send</Form.Button.Submit>
</Form>
PropTypeDescription
maxSubmitsnumberMax submits within the time window
windowMsnumberTime window in milliseconds

Behavior:

  • Tracks submit timestamps in sessionStorage
  • When limit is reached, shows a countdown alert
  • Graceful degradation: if sessionStorage is unavailable, submit is always allowed
  • Important: Must be paired with server-side rate limiting for real protection

Secure File Upload

Enhanced security for Form.Field.FileUpload with MIME verification, metadata stripping, and file renaming.

<Form.Field.FileUpload
  name="document"
  security={{
    maxSize: '10MB',
    allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
    stripMetadata: true,
    renameFile: true,
  }}
/>
PropTypeDescription
maxSizestring | numberMax file size ('10MB', '500KB', or bytes)
allowedTypesstring[]Allowed MIME types, checked via magic bytes
stripMetadatabooleanRemove EXIF data from images (Canvas re-encode)
renameFilebooleanReplace filename with UUID (path traversal protection)

MIME detection reads the first 4-8 bytes of the file to match against known signatures (magic bytes), not the file extension. Supported types: JPEG, PNG, GIF, WebP, PDF, ZIP.

EXIF stripping uses the Canvas API: the image is drawn onto a canvas and exported back, removing all metadata including GPS coordinates, camera info, etc.

File renaming replaces the original filename with a UUID while preserving the extension, preventing path traversal attacks like ../../etc/passwd.

Combining Security Features

All three can be used together:

<Form honeypot={true} rateLimit={{ maxSubmits: 5, windowMs: 300000 }} initialValue={data} onSubmit={handleSubmit}>
  <Form.Field.String name="name" />
  <Form.Field.String name="email" />
  <Form.Field.FileUpload
    name="avatar"
    security={{
      maxSize: '5MB',
      allowedTypes: ['image/*'],
      stripMetadata: true,
      renameFile: true,
    }}
  />
  <Form.Button.Submit>Submit</Form.Button.Submit>
</Form>

Utility Functions

These are exported for standalone use:

import { parseFileSize, validateMimeType, sanitizeFileName } from '@letar/forms'

parseFileSize('10MB') // 10485760
sanitizeFileName(file) // File with UUID name
await validateMimeType(file, ['image/jpeg']) // { valid: true, detectedMime: 'image/jpeg' }

Live Example

Try the interactive example on forms-example.letar.best.

On this page