Last updated: February 5, 2025

<span aria-hidden="true" id="secure-ajax-file-uploads-best-practices-and-techniques"></span>

# Secure Ajax file uploads: best practices and techniques

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_6nhQNS5wkVkPZWuKMzXcWrAHNJL5)

**Kevin van Zonneveld**

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

Securely handling file uploads is critical in modern web development. As AJAX file uploads become increasingly prevalent, protecting your application from potential vulnerabilities is essential. In this post, we explore modern best practices and techniques—including using the Fetch API, chunked uploads, and comprehensive error handling—to build secure file upload systems.

<span aria-hidden="true" id="understanding-ajax-file-uploads"></span>

## Understanding Ajax file uploads

AJAX (Asynchronous JavaScript and XML) enables web applications to send and receive data from the server asynchronously without refreshing the page. When implementing AJAX file uploads, it is important to address security considerations on both the client and server sides.

<span aria-hidden="true" id="browser-compatibility"></span>

## Browser compatibility

Modern browsers provide robust support for file uploads through various APIs:

* The FormData API is available in all modern browsers.
* The Fetch API is the recommended approach over legacy techniques.
* The File API offers advanced file handling capabilities.
* XMLHttpRequest remains supported for legacy scenarios but is generally discouraged in favor of Fetch.

For more details, refer to the MDN documentation on[FormData⁠](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and[Fetch API⁠](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI).

<span aria-hidden="true" id="implementing-modern-file-uploads"></span>

## Implementing modern file uploads

<span aria-hidden="true" id="using-the-fetch-api-with-asyncawait"></span>

### Using the fetch API with async/await

The Fetch API, combined with async/await, offers a clean method for uploading files:

```javascript
const uploadFile = async (file) => {
  const formData = new FormData()
  formData.append('file', file)

  try {
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData,
    })
    if (!response.ok) {
      throw new Error(`Upload failed: ${response.status}`)
    }
    return await response.json()
  } catch (error) {
    console.error('Upload error:', error)
    throw error
  }
}

```

<span aria-hidden="true" id="tracking-upload-progress"></span>

### Tracking upload progress

Using the Fetch API with streams allows for progress tracking during file uploads:

```javascript
const uploadWithProgress = async (file) => {
  const formData = new FormData()
  formData.append('file', file)

  try {
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData,
    })

    const reader = response.body.getReader()
    const contentLength = +response.headers.get('Content-Length')

    let receivedLength = 0
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      receivedLength += value.length
      const progress = (receivedLength / contentLength) * 100
      console.log(`Progress: ${progress.toFixed(2)}%`)
    }

    return await response.json()
  } catch (error) {
    console.error('Upload error:', error)
    throw error
  }
}

```

<span aria-hidden="true" id="handling-chunked-uploads"></span>

### Handling chunked uploads

For large files, breaking the upload into smaller chunks improves reliability in unstable network conditions:

```javascript
const CHUNK_SIZE = 1024 * 1024 // 1MB chunks

const uploadLargeFile = async (file) => {
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE)
  const fileId = Date.now().toString(36) // Simple unique ID

  for (let chunk = 0; chunk < totalChunks; chunk++) {
    const start = chunk * CHUNK_SIZE
    const end = Math.min(start + CHUNK_SIZE, file.size)
    const fileChunk = file.slice(start, end)
    const formData = new FormData()
    formData.append('chunk', fileChunk)
    formData.append('fileId', fileId)
    formData.append('chunkIndex', chunk)
    formData.append('totalChunks', totalChunks)

    try {
      await fetch('/upload/chunk', {
        method: 'POST',
        body: formData,
      })
    } catch (error) {
      console.error(`Chunk ${chunk} failed:`, error)
      throw error
    }
  }

  // Finalize the upload
  return fetch('/upload/complete', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ fileId }),
  })
}

```

<span aria-hidden="true" id="handling-multiple-file-uploads"></span>

## Handling multiple file uploads

Uploading multiple files concurrently can improve performance. By using the input's `multiple`attribute and modern JavaScript, you can efficiently handle several uploads:

```javascript
const uploadMultipleFiles = async (files) => {
  const uploadPromises = Array.from(files).map((file) => {
    const formData = new FormData()
    formData.append('file', file)
    return fetch('/upload', { method: 'POST', body: formData })
      .then((response) => {
        if (!response.ok) {
          throw new Error(`Upload failed for ${file.name}`)
        }
        return response.json()
      })
      .catch((error) => {
        console.error(`Error uploading ${file.name}:`, error)
        throw error
      })
  })
  return Promise.all(uploadPromises)
}

```

<span aria-hidden="true" id="drag-and-drop-file-upload"></span>

## Drag and drop file upload

Enhance user experience by implementing drag-and-drop file uploads. The following example establishes a drop zone that responds to drag events:

```javascript
const dropZone = document.getElementById('drop-zone')

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault()
  dropZone.classList.add('highlight')
})

dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('highlight')
})

dropZone.addEventListener('drop', async (e) => {
  e.preventDefault()
  dropZone.classList.remove('highlight')
  const files = e.dataTransfer.files
  try {
    const results = await uploadMultipleFiles(files)
    console.log('Files uploaded:', results)
  } catch (error) {
    console.error('Error during drag-and-drop upload:', error)
  }
})

```

<span aria-hidden="true" id="client-side-file-type-validation"></span>

## Client-side file type validation

Before uploading, validate file types using the File API to provide immediate feedback to users:

```javascript
const validateFileType = (file) => {
  const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf']
  if (!allowedTypes.includes(file.type)) {
    alert(`Invalid file type: ${file.type}`)
    return false
  }
  return true
}

const handleFileInput = (event) => {
  const files = event.target.files
  Array.from(files).forEach((file) => {
    if (validateFileType(file)) {
      uploadFile(file)
    }
  })
}

document.getElementById('file-input').addEventListener('change', handleFileInput)

```

<span aria-hidden="true" id="server-side-implementation"></span>

## Server-side implementation

Below is a Node.js example using Express and Multer for handling file uploads securely. For this example, we use Multer 1.4.5-lts.1 (latest stable as of 2024). Install it via: npm install multer\@1.4.5-lts.1

```javascript
const express = require('express')
const multer = require('multer') // using Multer 1.4.5-lts.1
const path = require('path')
const crypto = require('crypto')

const app = express()

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/')
  },
  filename: (req, file, cb) => {
    // Generate a secure random filename
    crypto.randomBytes(16, (err, raw) => {
      if (err) return cb(err)
      cb(null, raw.toString('hex') + path.extname(file.originalname))
    })
  },
})

const fileFilter = (req, file, cb) => {
  const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf']
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true)
  } else {
    cb(new Error('Invalid file type'), false)
  }
}

const upload = multer({
  storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5 MB
    files: 1,
  },
  fileFilter,
})

const rateLimit = require('express-rate-limit')
const uploadLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 uploads per window
})

app.post('/upload', uploadLimiter, upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      throw new Error('No file uploaded')
    }
    // Implement virus scanning here if needed
    res.json({
      message: 'File uploaded successfully',
      filename: req.file.filename,
    })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).json({
    error: 'Upload failed',
    details: err.message,
  })
})

```

<span aria-hidden="true" id="security-best-practices"></span>

## Security best practices

Protect your file upload system by implementing robust security measures:

1. Set a strict Content Security Policy (CSP):

```javascript
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'")
  next()
})

```

1. Validate uploaded files strictly:

```javascript
const validateFile = (file) => {
  const maxSize = 5 * 1024 * 1024 // 5MB
  const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf']
  if (file.size > maxSize) {
    throw new Error('File too large')
  }
  if (!allowedTypes.includes(file.type)) {
    throw new Error('Invalid file type')
  }
}

```

1. Configure secure storage:
   * Store files outside the web root.
   * Use randomized filenames.
   * Set proper file permissions.
   * Consider cloud storage options for scalability.
2. Implement rate limiting to curb abuse.
3. Enforce HTTPS to encrypt file transfers.
4. Use signed URLs for secure, direct-to-storage uploads.
5. Integrate virus scanning using reliable cloud-based solutions.

<span aria-hidden="true" id="error-handling-and-recovery"></span>

## Error handling and recovery

Robust error handling improves user experience. Implement retry logic for transient network failures:

```javascript
const uploadWithRetry = async (file, maxRetries = 3) => {
  let attempts = 0
  while (attempts < maxRetries) {
    try {
      const result = await uploadFile(file)
      return result
    } catch (error) {
      attempts++
      if (attempts === maxRetries) {
        throw new Error(`Upload failed after ${maxRetries} attempts`)
      }
      // Exponential backoff before retrying
      await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempts) * 1000))
    }
  }
}

```

<span aria-hidden="true" id="conclusion"></span>

## Conclusion

Secure file uploads require thorough client- and server-side measures. By implementing modern techniques for validation, error handling, and efficient uploads, you can build a robust system that mitigates common vulnerabilities.

For an advanced solution with extensive features, consider using [Uppy⁠](https://uppy.io/) (currently at version 4.x), which offers TypeScript support, Google Photos integration, React hooks, and parallel uploads.

\#ajax-file-upload#secure-ajax-uploads#javascript#web-security#handling-uploads-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed · 5 GB included in the free plan

Cancel anytime
