Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/secure-uploads/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Database connection string (SQLite - no external database server required)
# DATABASE_URL=file:./payload.db

# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE

# Used to configure CORS, format links and more. No trailing slash
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.example documents NEXT_PUBLIC_SERVER_URL, but the example payload.config.ts does not use it for cors/csrf (unlike other Next-based examples). Either wire this env var into the config or remove/adjust the comment to avoid suggesting configuration that has no effect.

Suggested change
# Used to configure CORS, format links and more. No trailing slash
# Used by the app (e.g. to format links). No trailing slash

Copilot uses AI. Check for mistakes.
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
14 changes: 14 additions & 0 deletions examples/secure-uploads/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
build
dist
node_modules
package-lock.json
.env
.next

# SQLite database
*.db
*.db-shm
*.db-wal

# Uploaded files
media/
30 changes: 30 additions & 0 deletions examples/secure-uploads/INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Installation Note

There is currently a Corepack signature verification issue preventing `pnpm` from working properly.
This is a known issue with Corepack in some environments.

## Workaround

You can install dependencies using npm instead:

```bash
cd examples/secure-uploads
npm install --legacy-peer-deps
```
Comment on lines +3 to +13
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This INSTALL note claims pnpm is currently broken due to a Corepack signature issue and recommends npm install --legacy-peer-deps. This is likely to become outdated quickly and it also conflicts with the README which instructs pnpm install. Recommend removing this file or replacing it with a short, evergreen troubleshooting note that links to a specific upstream issue.

Copilot uses AI. Check for mistakes.

Or fix the Corepack issue first:

```bash
# Update Corepack
corepack disable
corepack enable

# Then try pnpm again
pnpm install
```

Once dependencies are installed, you can run:

```bash
pnpm dev
```
124 changes: 124 additions & 0 deletions examples/secure-uploads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Payload CMS - Secure File Upload Example

This example demonstrates how to implement secure file uploads in Payload CMS using **[Pompelmi](https://github.com/jkomyno/pompelmi)**, an in-process security scanner.

## What This Example Does

This example showcases a Media collection with built-in security scanning for all uploaded files. Before any file is saved, it is automatically scanned for:

- **ZIP Bombs**: Malicious compressed files that expand to enormous sizes
- **Malware**: Suspicious files detected using YARA rules
- **Security Threats**: Other potential file-based attacks

If a security threat is detected, the upload is automatically rejected with a descriptive error message.

## Why Pompelmi?

**Pompelmi** is an open-source security scanning library that provides:

- ✅ **In-Process Scanning**: No cloud APIs or external services required
- ✅ **ZIP Bomb Detection**: Protects against decompression bombs
- ✅ **YARA Integration**: Industry-standard malware detection
- ✅ **Zero Latency**: Scans happen instantly during the upload process
- ✅ **Privacy-First**: Files never leave your server

### Recognition

Pompelmi has been featured in:

- **[Help Net Security](https://www.helpnetsecurity.com/)** - Leading cybersecurity news source
- **[Bytes.dev](https://bytes.dev/)** - Popular web development newsletter

## How It Works

The security scanning is implemented in the Media collection's `beforeOperation` hook:

```typescript
import { scanBytes } from 'pompelmi'
import { APIError } from 'payload'

hooks: {
beforeOperation: [
async ({ operation, req }) => {
if ((operation === 'create' || operation === 'update') && req.file?.data) {
const result = await scanBytes(req.file.data)

if (result.verdict !== 'clean') {
throw new APIError(
`Security Check Failed: ${result.reasons?.join(', ') || result.verdict}`,
400
)
}
}
},
],
}
```

## Setup Instructions

1. **Clone and Install Dependencies**

\`\`\`bash
cd examples/secure-uploads
pnpm install
\`\`\`

2. **Set Up Environment Variables**

Copy the example environment file:

\`\`\`bash
cp .env.example .env
\`\`\`

Update the following variables:

- \`DATABASE_URL\`: Your MongoDB connection string
- \`PAYLOAD_SECRET\`: A secure random string for JWT encryption

Comment on lines +75 to +79
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README setup instructions mention setting DATABASE_URL to a MongoDB connection string, but this example’s config uses the SQLite adapter and never reads process.env.DATABASE_URL. Update the README to describe SQLite setup (or switch the config to read DATABASE_URL) so the instructions match the actual runtime behavior.

Copilot uses AI. Check for mistakes.
3. **Start the Development Server**

\`\`\`bash
pnpm dev
\`\`\`

4. **Access the Application**
- Frontend: [http://localhost:3000](http://localhost:3000)
- Admin Panel: [http://localhost:3000/admin](http://localhost:3000/admin)

## Testing the Security Scanner

1. Navigate to the admin panel at `/admin`
2. Create a user account if you haven't already
3. Go to the "Media" collection
4. Try uploading various files:
- ✅ Normal images and documents will be accepted
- ❌ Malicious files or ZIP bombs will be rejected with an error

## Key Files

- **[src/collections/Media.ts](src/collections/Media.ts)** - Media collection with security scanning hook
- **[src/collections/Users.ts](src/collections/Users.ts)** - Basic user authentication
- **[src/payload.config.ts](src/payload.config.ts)** - Main Payload configuration
- **[package.json](package.json)** - Dependencies including Pompelmi

## Learn More

- **Pompelmi Documentation**: [https://github.com/jkomyno/pompelmi](https://github.com/jkomyno/pompelmi)
- **Payload Hooks**: [https://payloadcms.com/docs/hooks/overview](https://payloadcms.com/docs/hooks/overview)
- **Payload Upload Fields**: [https://payloadcms.com/docs/upload/overview](https://payloadcms.com/docs/upload/overview)

## Production Considerations

When deploying to production:

1. **Performance**: Pompelmi scanning is fast, but consider the impact on large file uploads
2. **Logging**: The example logs scan results - configure proper monitoring
3. **Error Handling**: Customize error messages for your users
4. **YARA Rules**: Consider customizing or extending the YARA rules for your specific needs
5. **File Size Limits**: Set appropriate upload size limits in your Payload configuration

## License

MIT
5 changes: 5 additions & 0 deletions examples/secure-uploads/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
8 changes: 8 additions & 0 deletions examples/secure-uploads/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { withPayload } from '@payloadcms/next/withPayload'

/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
}

export default withPayload(nextConfig)
35 changes: 35 additions & 0 deletions examples/secure-uploads/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "payload-example-secure-uploads",
"version": "1.0.0",
"description": "Payload example demonstrating secure file uploads with Pompelmi security scanning",
"license": "MIT",
"type": "module",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/db-mongodb": "latest",
"@payloadcms/db-sqlite": "^3.76.0",
"@payloadcms/next": "latest",
"@payloadcms/richtext-lexical": "latest",
"@payloadcms/ui": "latest",
"cross-env": "^7.0.3",
"graphql": "16.8.1",
"next": "^15.4.10",
"payload": "latest",
"pompelmi": "^0.29.1",
"react": "19.2.1",
"react-dom": "19.2.1"
},
"devDependencies": {
"@types/node": "^18.11.5",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"typescript": "^5.7.2"
}
}
7 changes: 7 additions & 0 deletions examples/secure-uploads/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
25 changes: 25 additions & 0 deletions examples/secure-uploads/src/app/(app)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default function Home() {
return (
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
<h1>Payload CMS - Secure File Upload Example</h1>
<p>
This example demonstrates how to implement secure file uploads using{' '}
<strong>Pompelmi</strong>, an in-process security scanner.
</p>
<p>
Go to <a href="/admin">/admin</a> to access the Payload admin panel and try uploading files
to the Media collection.
</p>
<h2>Features:</h2>
<ul>
<li>ZIP bomb detection</li>
<li>YARA-based malware scanning</li>
<li>In-process scanning (no cloud APIs required)</li>
<li>Automatic rejection of malicious files</li>
</ul>
<p>
Learn more about <a href="https://github.com/jkomyno/pompelmi">Pompelmi on GitHub</a>.
</p>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'

import config from '@payload-config'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'

type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}

export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })

const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap })

export default NotFound
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'

import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'

type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}

export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })

const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })

export default Page
2 changes: 2 additions & 0 deletions examples/secure-uploads/src/app/(payload)/admin/importMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
export const importMap = {}
19 changes: 19 additions & 0 deletions examples/secure-uploads/src/app/(payload)/api/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes'

export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const PUT = REST_PUT(config)
export const OPTIONS = REST_OPTIONS(config)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_GET, GRAPHQL_POST } from '@payloadcms/next/routes'

export const GET = GRAPHQL_GET(config)
export const POST = GRAPHQL_POST(config)
1 change: 1 addition & 0 deletions examples/secure-uploads/src/app/(payload)/custom.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Add custom SCSS here
31 changes: 31 additions & 0 deletions examples/secure-uploads/src/app/(payload)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import type { ServerFunctionClient } from 'payload'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'

import { importMap } from './admin/importMap.js'
import './custom.scss'

type Args = {
children: React.ReactNode
}

const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}

const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)

export default Layout
Loading
Loading