Lightweight and extendable body-parser for express style web frameworks
Installation
npm install modular-body
Basic usage:
importexpressfrom'express';import{bodyParser}from'modular-body';constapp=express();app.use(bodyParser());// createse parsers for 'application/json', 'text/plain',// 'application/octet-stream' and 'application/x-www-form-urlencoded'app.listen(3000,()=>console.log('Server running on port 3000'));
Please be aware that the prevention of prototype poisoning is only implemented for the first
level of an object converted from JSON, which is sufficient if we do not use Object.assign
with nested child objects. If it is necessary also for nested objects, the implementation
is shown further down in this readme.
importexpressfrom'express';import{bodyParser}from'modular-body';constapp=express();app.use(bodyParser.json());// create parser only for 'application/json'app.listen(3000,()=>console.log('Server running on port 3000'));
Extended usage:
importexpressfrom'express';import{bodyParser,ParserConfigurations,}from'modular-body';import{Buffer}from'buffer';import{MediaType}from'./mediaTypes';constparserConfigurations: ParserConfigurations=['text/plain',// use default configuration{// change default configuration, not specified fields will be used from options entries// or from default configuration of parserinflate: ['identity','gzip','br'],limit: '20Mb',// use custom limitdefaultEncoding: 'ucs-2',// this encoding will be used when charset is not specifiedencodings: ['latin1'],// add additional encodings which should be used for this parsermatcher: 'application/json',parser: (payload: string)=>JSON.stringify(payload,null,2),},{inflate: true,// allow 'identity', 'inflate', 'gzip' and 'br'matcher: 'application/octet-stream',},{// create individual parser configurationinflate: 'identity',// allow no compressionlimit: 1000000,matcher: ['image/jpeg','image/png','mpeg/*',(mediaType: MediaType)=>mediaType[0]==='image'&&mediaType[1].match(/+xml$/),],parser: (payload: Buffer)=>'Creating image...',},];constapp=express();app.use(bodyParser({defaultLimit: '100kb',inflate: ['identity','br']},parserConfigurations));app.listen(3000,()=>console.log('Server running on port 3000'));
Extending for special requirements
For extending equivalent to the npm package body-parser see the tests in rawBodyParser.test.ts, textBodyParser.test.ts, jsonBodyParser.test.ts
and urlencodedBodyParser.test.ts.
importexpressfrom'express';import{bodyParser,BufferEncodings,ParserConfigurations}from'modular-body';importbase62strfrom'base62str';importLZWDecoderfrom'lzw-stream/decoder';import{Buffer}from'buffer';constbufferEncodings=[{encodings: ['base62','base-62'],transform: (buffer: Buffer)=>base62str.decodeStr(buffer.toString()),}];constdecompressors={'lzw': ()=>newLZWDecoder,};constapp=express();app.use(bodyParser({inflate: true},undefined,bufferEncodings,decompressors));app.listen(3000,()=>console.log('Server running on port 3000'));
Preventing prototype poisoning on nested parsed JSON objects
This is an example code to prevent prototype poisoning. The default implementation checks
only the existence of a __proto__ key only for the keys in the first object level because
of speed considerations. If the usage of the __proto__ key should be prevented for all
nested objects then this parser configuration could be used.
importexpressfrom'express';import{bodyParser,ParserConfigurations,}from'modular-body';typeJSONValue=|string|number|boolean|{[x: string]: JSONValue}|Array<JSONValue>;/** * Adapted from * https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects * @param jsonValue The object from the parsed JSON string * @param key The key which should be found in the object */functionkeyExistsInNestedObject(jsonValue: JSONValue,key: string){constallLists: (null|JSONValue[])[]=[];constallArray: (null|JSONValue[])[]=[];if(typeofjsonValue!=='object'||jsonValue===null){returnfalse;}if(Array.isArray(jsonValue)){allArray.push(jsonValue);}else{if(Object.keys(jsonValue).includes(key)){returntrue;}allLists.push(Object.values(jsonValue));}letallListsSize=allLists.length;letallArraySize=allArray.length;letindexLists=0;letindexArray=0;do{for(;indexArray<allArraySize;indexArray=indexArray+1){constcurrentArray=allArray[indexArray];constcurrentLength=(<JSONValue[]>currentArray).length;for(leti=0;i<currentLength;i+=1){constarrayItemInner=(<JSONValue[]>currentArray)[i];if(typeofarrayItemInner==='object'&&arrayItemInner!==null){if(Array.isArray(arrayItemInner)){allArraySize=allArray.push(arrayItemInner);}else{if(Object.keys(arrayItemInner).includes(key)){returntrue;}allListsSize=allLists.push(Object.values(arrayItemInner));}}}allArray[indexArray]=null;}for(;indexLists<allListsSize;indexLists=indexLists+1){constcurrentList=allLists[indexLists];constcurrentLength=(<JSONValue[]>currentList).length;for(leti=0;i<currentLength;i+=1){constlistItemInner=(<JSONValue[]>currentList)[i];if(typeoflistItemInner==='object'&&listItemInner!==null){if(Array.isArray(listItemInner)){allArraySize=allArray.push(listItemInner);}else{if(Object.keys(listItemInner).includes(key)){returntrue;}allListsSize=allLists.push(Object.values(listItemInner));}}}allLists[indexLists]=null;}}while(indexLists<allListsSize||indexArray<allArraySize);returnfalse;}constparserConfiguration=<ParserConfigurations<string,JSONValue>>{matcher: 'application/json',parser: (payload: string)=>{constjsonObject: JSONValue=JSON.parse(payload);if(typeofjsonObject==='object'&&payload.includes('"__proto__":')&&keyExistsInNestedObject(jsonObject,'__proto__')){thrownewError('Using "__proto__" as JSON key is not allowed.');}returnjsonObject;},defaultEncoding: 'utf-8',emptyResponse: {},};constapp=express();app.use(bodyParser({defaultLimit: '100kb',inflate: ['identity','br']},parserConfiguration));app.listen(3000,()=>console.log('Server running on port 3000'));
Type DefaultOptions
Properties
Name
Type
Details
defaultLimit
number | string
The default limit which should be set on the parser configurations, default is '20kb'.
inflate
true | string | string[]
When true allows all available decompressors, otherwise only specified decompressors. Default is 'identity'.
requireContentLength
boolean
Set if the 'Content-Length' header has to be set, default is false.
defaultContentType
string
When 'Content-Type' header is missing, then use this as default. Default is not set which will throw when header is missing.
Type ParserConfiguration<U, V>
Properties
Name
Type
Details
inflate
true | string | string[]
Add the allowed decompressors(s) as string or array, allow all decompressors with true
limit
string | number
Specify the maximum allowed body size as a number in bytes or as a byte string
requireContentLength
boolean
Specify if the header 'Content-Length' has to be set on the request
parser
((payload: Buffer | U) => V) | null
A function to parse the payload from the buffer or after encoding
matcher
MediaTypeIdentifier | MediaTypeIdentifier[]
The matchers for the allowed mime types as a matching function or mime type where '*' is allowed on either side of the slash to matches all.
encodings
string | string[] | boolean | null
Allow the specified encoding(s), allow all with true, remove/prevent with false or null from default config, no encoding with undefined
defaultEncoding
string
The encoding which should be used when 'charset' is not set on the 'Content-Type' header