-
Notifications
You must be signed in to change notification settings - Fork 3.4k
feat(examples): add secure-uploads example with Pompelmi security scanning #15571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| NEXT_PUBLIC_SERVER_URL=http://localhost:3000 | ||
| 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/ |
| 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
|
||
|
|
||
| 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 | ||
| ``` | ||
| 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
|
||
| 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 | ||
| 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. |
| 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) |
| 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" | ||
| } | ||
| } |
| 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> | ||
| ) | ||
| } |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ | ||
| export const importMap = {} |
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| // Add custom SCSS here |
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.env.exampledocumentsNEXT_PUBLIC_SERVER_URL, but the examplepayload.config.tsdoes not use it forcors/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.