Skip to content

Commit

Permalink
allow user to provide resource parameter value validator; escape reso…
Browse files Browse the repository at this point in the history
…urce name in error output
  • Loading branch information
lingyan committed Jan 13, 2016
1 parent 1620c52 commit 11f1229
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*.yml
.editorconfig
/docs/
/artifacts/
/examples/
/tests/
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,9 @@ fetcher
.end(callbackFn);
```

## Security

## CSRF Protection
### CSRF Protection

You can protect your XHR paths from CSRF attacks by adding a middleware in front of the fetchr middleware:

Expand All @@ -349,6 +350,26 @@ var fetcher = new Fetcher({

This `_csrf` will be sent in all XHR requests as a query parameter so that it can be validated on the server.

### Resource Parameter Validation

You can provide your custom resource parameter value validator as for filtering/escaping resource parameter values.

`paramValueValidator` is a function that can be passed into the `Fetcher.middleware` method. It is passed two arguments, the parameter value (can be any object) and the parameter name. The `paramValueValidator` function is expected to return the validated parameter value.

Take a look at the example below:

```js
/**
Using the app.js from above, you can modify the Fetcher.middleware
method to pass in the responseFormatter function.
*/
app.use('/myCustomAPIEndpoint', Fetcher.middleware({
paramValueValidator: function (value, name) {
// return validatedValue;
}
}));
```

## Service Call Config

When calling a Fetcher service you can pass an optional config object.
Expand Down
20 changes: 16 additions & 4 deletions libs/fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ function parseValue(value) {
}
}

function parseParamValues (params) {
function parseParamValues (params, paramValueValidator) {
var shouldValidate = (typeof paramValueValidator === 'function');
return Object.keys(params).reduce(function (parsed, curr) {
parsed[curr] = parseValue(params[curr]);
if (shouldValidate) {
parsed[curr] = paramValueValidator(parsed[curr], curr);
}
return parsed;
}, {});
}

function escapeResource(resource) {
return resource.replace(/[^\w\.]+/g, '');
}

/**
* Takes an error and resolves output and statusCode to respond to client with
*
Expand Down Expand Up @@ -288,6 +296,10 @@ Fetcher.isRegistered = function (name) {
* @method middleware
* @memberof Fetcher
* @param {Object} [options] Optional configurations
* @param {Function} [options.paramValueValidator] Optional function to validate the resource
parameter values. First argument is the value object, which can be string, array, object.
This function is expected to return the validated value. Second argument is the
name of the resource parameter.
* @param {Function} [options.responseFormatter=no op function] Function to modify the response
before sending to client. First argument is the HTTP request object,
second argument is the HTTP response object and the third argument is the service data object.
Expand All @@ -311,7 +323,7 @@ Fetcher.middleware = function (options) {

if (!Fetcher.isRegistered(resource)) {
error = fumble.http.badRequest('Invalid Fetchr Access', {
debug: 'Bad resource ' + resource
debug: 'Bad resource ' + escapeResource(resource)
});
error.source = 'fetchr';
return next(error);
Expand All @@ -322,7 +334,7 @@ Fetcher.middleware = function (options) {
serviceMeta: serviceMeta
});
request
.params(parseParamValues(qs.parse(path.join('&'))))
.params(parseParamValues(qs.parse(path.join('&')), options.paramValueValidator))
.end(function (err, data) {
var meta = serviceMeta[0] || {};
if (meta.headers) {
Expand Down Expand Up @@ -359,7 +371,7 @@ Fetcher.middleware = function (options) {

if (!Fetcher.isRegistered(singleRequest.resource)) {
error = fumble.http.badRequest('Invalid Fetchr Access', {
debug: 'Bad resource ' + singleRequest.resource
debug: 'Bad resource ' + escapeResource(singleRequest.resource)
});
error.source = 'fetchr';
return next(error);
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/libs/fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,54 @@ describe('Server Fetcher', function () {
middleware(req, res, next);
});

it('should allow resource param validation', function (done) {
var operation = 'read',
statusCodeSet = false,
params = {
foo: 'bar'
},
req = {
method: 'GET',
path: '/' + mockService.name + ';' + qs.stringify(params, ';'),
query: {
returnMeta: true
}
},
res = {
json: function(response) {
expect(response).to.exist;
expect(response).to.not.be.empty;
expect(response).to.contain.keys('data', 'meta');
expect(response.data).to.contain.keys('operation', 'args');
expect(response.data.operation.name).to.equal(operation);
expect(response.data.operation.success).to.be.true;
expect(response.data.args).to.contain.keys('params');
expect(response.data.args.params.foo).to.equal('validated-foo-bar');
expect(statusCodeSet).to.be.true;
done();
},
status: function(code) {
expect(code).to.equal(200);
statusCodeSet = true;
return this;
},
send: function (code) {
console.log('Not Expected: middleware responded with', code);
}
},
next = function () {
console.log('Not Expected: middleware skipped request');
},
middleware = Fetcher.middleware({
pathPrefix: '/api',
paramValueValidator: function (value, name) {
return 'validated-' + name + '-' + value;
}
});

middleware(req, res, next);
});

var paramsToQuerystring = function(params) {
var str = '';
for (var key in params) {
Expand Down Expand Up @@ -552,6 +600,9 @@ describe('Server Fetcher', function () {
it('should skip invalid GET resource', function (done) {
makeInvalidReqTest({method: 'GET', path: '/invalidService'}, 'Bad resource invalidService', done);
});
it('should escape resource name for invalid GET resource', function (done) {
makeInvalidReqTest({method: 'GET', path: '/invalid&Service'}, 'Bad resource invalidService', done);
});
it('should skip invalid POST request', function (done) {
makeInvalidReqTest({method: 'POST', body: {
requests: {
Expand Down

0 comments on commit 11f1229

Please sign in to comment.