Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: fastify/fastify-multipart
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v8.0.0
Choose a base ref
...
head repository: fastify/fastify-multipart
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v8.1.0
Choose a head ref
  • 15 commits
  • 11 files changed
  • 6 contributors

Commits on Sep 28, 2023

  1. remove deps to dev (#483)

    gurgunday authored Sep 28, 2023
    Copy the full SHA
    e487c13 View commit details

Commits on Sep 29, 2023

  1. fix eslint (#485)

    gurgunday authored Sep 29, 2023
    Copy the full SHA
    109a116 View commit details

Commits on Sep 30, 2023

  1. use in to check for prototype violation (#484)

    * use in to check for prototype violation
    
    * add additional prototype checks
    gurgunday authored Sep 30, 2023
    Copy the full SHA
    bf18acc View commit details

Commits on Oct 7, 2023

  1. Copy the full SHA
    10ff992 View commit details

Commits on Oct 8, 2023

  1. Copy the full SHA
    9ab7fbe View commit details

Commits on Oct 16, 2023

  1. chore(deps-dev): bump climem from 1.0.3 to 2.0.0 (#490)

    Bumps [climem](https://github.com/mcollina/climem) from 1.0.3 to 2.0.0.
    - [Release notes](https://github.com/mcollina/climem/releases)
    - [Commits](https://github.com/mcollina/climem/commits/v2.0.0)
    
    ---
    updated-dependencies:
    - dependency-name: climem
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Oct 16, 2023
    Copy the full SHA
    e497c21 View commit details

Commits on Oct 23, 2023

  1. docs: update for attachFieldsToBody: keyValues (#492)

    * docs: update for attachFieldsToBody: keyValues
    
    * fix docs to match fastify style guide
    dancastillo authored Oct 23, 2023
    Copy the full SHA
    9ff3c28 View commit details
  2. add parts type and enable coverage checking (#491)

    * add parts type
    
    * enable coverage check
    
    * test if flaky
    
    * Revert "test if flaky"
    
    This reverts commit 76388dd.
    
    * ignore line
    
    * add why we're ignoring else
    gurgunday authored Oct 23, 2023
    Copy the full SHA
    595583e View commit details

Commits on Oct 27, 2023

  1. Copy the full SHA
    e2a4eba View commit details

Commits on Dec 11, 2023

  1. chore(deps-dev): bump @fastify/swagger-ui from 1.10.2 to 2.0.1 (#499)

    Bumps [@fastify/swagger-ui](https://github.com/fastify/fastify-swagger-ui) from 1.10.2 to 2.0.1.
    - [Release notes](https://github.com/fastify/fastify-swagger-ui/releases)
    - [Commits](fastify/fastify-swagger-ui@v1.10.2...v2.0.1)
    
    ---
    updated-dependencies:
    - dependency-name: "@fastify/swagger-ui"
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 11, 2023
    Copy the full SHA
    96defa4 View commit details

Commits on Dec 18, 2023

  1. chore(deps-dev): bump tsd from 0.29.0 to 0.30.0 (#500)

    Bumps [tsd](https://github.com/tsdjs/tsd) from 0.29.0 to 0.30.0.
    - [Release notes](https://github.com/tsdjs/tsd/releases)
    - [Commits](tsdjs/tsd@v0.29.0...v0.30.0)
    
    ---
    updated-dependencies:
    - dependency-name: tsd
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 18, 2023
    Copy the full SHA
    b3f1813 View commit details

Commits on Dec 25, 2023

  1. chore(deps-dev): bump readable-stream from 3.6.2 to 4.5.1 (#501)

    Bumps [readable-stream](https://github.com/nodejs/readable-stream) from 3.6.2 to 4.5.1.
    - [Release notes](https://github.com/nodejs/readable-stream/releases)
    - [Commits](nodejs/readable-stream@v3.6.2...v4.5.1)
    
    ---
    updated-dependencies:
    - dependency-name: readable-stream
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 25, 2023
    Copy the full SHA
    fb63d80 View commit details

Commits on Jan 1, 2024

  1. Copy the full SHA
    2430619 View commit details

Commits on Jan 6, 2024

  1. Bumped v8.0.0

    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    mcollina committed Jan 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a1e4c82 View commit details
  2. Bumped v8.1.0

    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    mcollina committed Jan 6, 2024
    Copy the full SHA
    be610c5 View commit details
Showing with 444 additions and 84 deletions.
  1. +23 −49 .eslintrc.json
  2. +2 −0 .gitattributes
  3. +0 −1 .taprc
  4. +2 −4 README.md
  5. +11 −6 index.js
  6. +7 −6 package.json
  7. +135 −0 test/multipart-security.test.js
  8. +221 −0 test/multipart-update-options-type.test.js
  9. +9 −12 tsconfig.eslint.json
  10. +8 −2 types/index.d.ts
  11. +26 −4 types/index.test-d.ts
72 changes: 23 additions & 49 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,52 +1,26 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"standard"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"env": { "node": true },
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"project": "./tsconfig.eslint.json",
"createDefaultProgram": true
},
"rules": {
"no-console": "off",
"@typescript-eslint/indent": ["error", 2],
"semi": ["error", "never"],
"import/export": "off" // this errors on multiple exports (overload interfaces)
},
"overrides": [
{
"files": ["*.d.ts","*.test-d.ts"],
"rules": {
"no-use-before-define": "off",
"no-redeclare": "off",
"@typescript-eslint/no-explicit-any": "off"
}
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "standard"],
"overrides": [
{
"files": ["types/*.test-d.ts", "types/*.d.ts"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.eslint.json"]
},
{
"files": ["*.test-d.ts"],
"rules": {
"no-unused-expressions": "off",
"@typescript-eslint/no-var-requires": "off",
"no-unused-vars": "off",
"n/handle-callback-err": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-misused-promises": ["error", {
"checksVoidReturn": false
}]
},
"globals": {
"NodeJS": "readonly"
}
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"no-unused-expressions": "off",
"no-use-before-define": "off",
"no-redeclare": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unused-vars": "off"
}
]
}
}
]
}
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf
1 change: 0 additions & 1 deletion .taprc
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
check-coverage: false
files:
- test/**/*.test.js
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -288,7 +288,7 @@ If you try to read from a stream and pipe to a new file, you will obtain an empt

## JSON Schema body validation

If you enable `attachFieldsToBody: 'keyValues'` then the response body and JSON Schema validation will behave similarly to `application/json` and [`application/x-www-form-urlencoded`](https://github.com/fastify/fastify-formbody) content types. Files will be decoded using `Buffer.toString()` and attached as a body value.
When the `attachFieldsToBody` parameter is set to `'keyValues'`, JSON Schema validation on the body will behave similarly to `application/json` and [`application/x-www-form-urlencoded`](https://github.com/fastify/fastify-formbody) content types. Additionally, uploaded files will be attached to the body as `Buffer` objects.

```js
fastify.register(require('@fastify/multipart'), { attachFieldsToBody: 'keyValues' })
@@ -302,9 +302,7 @@ fastify.post('/upload/files', {
properties: {
// file that gets decoded to string
myFile: {
type: 'string',
// validate that file contents match a UUID
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
type: 'object',
},
hello: {
type: 'string',
17 changes: 11 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -17,7 +17,6 @@ const secureJSON = require('secure-json-parse')

const kMultipart = Symbol('multipart')
const kMultipartHandler = Symbol('multipartHandler')
const getDescriptor = Object.getOwnPropertyDescriptor

const PartsLimitError = createError('FST_PARTS_LIMIT', 'reach parts limit', 413)
const FilesLimitError = createError('FST_FILES_LIMIT', 'reach files limit', 413)
@@ -29,7 +28,7 @@ const InvalidJSONFieldError = createError('FST_INVALID_JSON_FIELD_ERROR', 'a req
const FileBufferNotFoundError = createError('FST_FILE_BUFFER_NOT_FOUND', 'the file buffer was not found', 500)

function setMultipart (req, payload, done) {
req.raw[kMultipart] = true
req[kMultipart] = true
done()
}

@@ -95,6 +94,8 @@ function fastifyMultipart (fastify, options, done) {
const key = reqBodyKeys[i]
const field = req.body[key]

/* Don't modify the body if a field doesn't have a value or an attached buffer */
/* istanbul ignore else */
if (field.value !== undefined) {
body[key] = field.value
} else if (field._buf) {
@@ -139,6 +140,7 @@ function fastifyMultipart (fastify, options, done) {
})

fastify.addContentTypeParser('multipart/form-data', setMultipart)
fastify.decorateRequest(kMultipart, false)
fastify.decorateRequest(kMultipartHandler, handleMultipart)

fastify.decorateRequest('parts', getMultipartIterator)
@@ -160,7 +162,7 @@ function fastifyMultipart (fastify, options, done) {
})

function isMultipart () {
return this.raw[kMultipart]
return this[kMultipart]
}

function handleMultipart (opts = {}) {
@@ -249,7 +251,7 @@ function fastifyMultipart (fastify, options, done) {

function onField (name, fieldValue, fieldnameTruncated, valueTruncated, encoding, contentType) {
// don't overwrite prototypes
if (getDescriptor(Object.prototype, name)) {
if (name in Object.prototype) {
onError(new PrototypeViolationError())
return
}
@@ -295,7 +297,7 @@ function fastifyMultipart (fastify, options, done) {

function onFile (name, file, filename, encoding, mimetype) {
// don't overwrite prototypes
if (getDescriptor(Object.prototype, name)) {
if (name in Object.prototype) {
// ensure that stream is consumed, any error is suppressed
sendToWormhole(file)
onError(new PrototypeViolationError())
@@ -453,7 +455,8 @@ function fastifyMultipart (fastify, options, done) {
try {
await unlink(filepath)
} catch (error) {
this.log.error(error, 'could not delete file')
/* istanbul ignore next */
this.log.error(error, 'Could not delete file')
}
}
}
@@ -462,6 +465,8 @@ function fastifyMultipart (fastify, options, done) {
const parts = this[kMultipartHandler](options)
let part
while ((part = await parts()) != null) {
/* Only return a part if the file property exists */
/* istanbul ignore else */
if (part.file) {
return part
}
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
{
"name": "@fastify/multipart",
"version": "7.7.3",
"version": "8.1.0",
"description": "Multipart plugin for Fastify",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"dependencies": {
"@fastify/busboy": "^1.0.0",
"@fastify/deepmerge": "^1.0.0",
"@fastify/error": "^3.0.0",
"@fastify/swagger": "^8.3.1",
"@fastify/swagger-ui": "^1.8.0",
"fastify-plugin": "^4.0.0",
"secure-json-parse": "^2.4.0",
"stream-wormhole": "^1.1.0"
},
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"@fastify/swagger": "^8.10.1",
"@fastify/swagger-ui": "^2.0.1",
"@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"benchmark": "^2.1.4",
"climem": "^1.0.3",
"climem": "^2.0.0",
"concat-stream": "^2.0.0",
"eslint": "^8.20.0",
"eslint-plugin-import": "^2.26.0",
@@ -31,11 +32,11 @@
"h2url": "^0.2.0",
"noop-stream": "^0.1.0",
"pump": "^3.0.0",
"readable-stream": "^3.6.0",
"readable-stream": "^4.5.1",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"tap": "^16.0.0",
"tsd": "^0.29.0"
"tsd": "^0.30.0"
},
"scripts": {
"coverage": "npm run test:unit -- --coverage-report=html",
135 changes: 135 additions & 0 deletions test/multipart-security.test.js
Original file line number Diff line number Diff line change
@@ -104,6 +104,141 @@ test('should not allow __proto__ as field name', function (t) {
})
})

test('should not allow toString as field name', function (t) {
t.plan(4)

const fastify = Fastify()
t.teardown(fastify.close.bind(fastify))

fastify.register(multipart)

fastify.post('/', async function (req, reply) {
t.ok(req.isMultipart())

try {
await req.file()
reply.code(200).send()
} catch (error) {
t.ok(error instanceof fastify.multipartErrors.PrototypeViolationError)
reply.code(500).send()
}
})

fastify.listen({ port: 0 }, async function () {
// request
const form = new FormData()
const opts = {
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
}

const req = http.request(opts, (res) => {
t.equal(res.statusCode, 500)
res.resume()
res.on('end', () => {
t.pass('res ended successfully')
})
})
form.append('toString', 'world')

form.pipe(req)
})
})

test('should not allow hasOwnProperty as field name', function (t) {
t.plan(4)

const fastify = Fastify()
t.teardown(fastify.close.bind(fastify))

fastify.register(multipart)

fastify.post('/', async function (req, reply) {
t.ok(req.isMultipart())

try {
await req.file()
reply.code(200).send()
} catch (error) {
t.ok(error instanceof fastify.multipartErrors.PrototypeViolationError)
reply.code(500).send()
}
})

fastify.listen({ port: 0 }, async function () {
// request
const form = new FormData()
const opts = {
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
}

const req = http.request(opts, (res) => {
t.equal(res.statusCode, 500)
res.resume()
res.on('end', () => {
t.pass('res ended successfully')
})
})
form.append('hasOwnProperty', 'world')

form.pipe(req)
})
})

test('should not allow propertyIsEnumerable as field name', function (t) {
t.plan(4)

const fastify = Fastify()
t.teardown(fastify.close.bind(fastify))

fastify.register(multipart)

fastify.post('/', async function (req, reply) {
t.ok(req.isMultipart())

try {
await req.file()
reply.code(200).send()
} catch (error) {
t.ok(error instanceof fastify.multipartErrors.PrototypeViolationError)
reply.code(500).send()
}
})

fastify.listen({ port: 0 }, async function () {
// request
const form = new FormData()
const opts = {
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
}

const req = http.request(opts, (res) => {
t.equal(res.statusCode, 500)
res.resume()
res.on('end', () => {
t.pass('res ended successfully')
})
})
form.append('propertyIsEnumerable', 'world')

form.pipe(req)
})
})

test('should use default for fileSize', async function (t) {
t.plan(4)

Loading