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.
'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>
)
}shadcn-вариант этого примера ещё не готов.
Vue-пример для этого поля появится позже.
Angular-пример для этого поля появится позже.
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>withdisplay: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>| Prop | Type | Description |
|---|---|---|
maxSubmits | number | Max submits within the time window |
windowMs | number | Time window in milliseconds |
Behavior:
- Tracks submit timestamps in
sessionStorage - When limit is reached, shows a countdown alert
- Graceful degradation: if
sessionStorageis 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,
}}
/>| Prop | Type | Description |
|---|---|---|
maxSize | string | number | Max file size ('10MB', '500KB', or bytes) |
allowedTypes | string[] | Allowed MIME types, checked via magic bytes |
stripMetadata | boolean | Remove EXIF data from images (Canvas re-encode) |
renameFile | boolean | Replace 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.