diff --git a/.gitignore b/.gitignore index 61e7b17e1..0801e9c25 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .nyc_output coverage /test/fixtures/certs/tmp +accept.json diff --git a/client-templates/github/.env.sample b/client-templates/github/.env.sample index 0b5c2f533..c4e738258 100644 --- a/client-templates/github/.env.sample +++ b/client-templates/github/.env.sample @@ -12,6 +12,10 @@ GITHUB= # changed to "api.github.com" GITHUB_API=$GITHUB/api/v3 +# the url that the github graphql API should be accessed at. +# For github.com this should be changed to "api.github.com/graphql" +GITHUB_GRAPHQL=$GITHUB/api/graphql + # the url of your broker client (including scheme and port) # BROKER_CLIENT_URL= diff --git a/client-templates/github/accept.json.sample b/client-templates/github/accept.json.sample index b5ca4e923..3b5e94e45 100644 --- a/client-templates/github/accept.json.sample +++ b/client-templates/github/accept.json.sample @@ -253,6 +253,19 @@ "method": "PATCH", "path": "/repos/:name/:repo/git/refs/heads/:ref", "origin": "https://${GITHUB_TOKEN}@${GITHUB_API}" + }, + { + "//": "query graphql", + "method": "POST", + "path": "/graphql", + "origin": "https://${GITHUB_TOKEN}@${GITHUB_GRAPHQL}", + "valid": [ + { + "//": "query for all the file names 2 levels deep in the repo. used for auto project detection", + "path": "query", + "regex": "{\\s*repositoryOwner\\(login:\\s*\"([a-zA-Z0-9-_.]+)\"\\)\\s*{\\s*repository\\(name:\\s*\"([a-zA-Z0-9-_.]+)\"\\)\\s*{\\s*object\\(expression:\\s*\"([^\"{}]+)\"\\)\\s*{\\s*\\.\\.\\.\\s*on\\s+Tree\\s+{\\s*entries\\s*{\\s*name\\s+type\\s+object\\s*{\\s*\\.\\.\\.\\s*on\\s+Tree\\s+{\\s*entries\\s*{\\s*name\\s+type\\s+object\\s*{\\s*\\.\\.\\.on\\s+Tree\\s*{\\s*entries\\s*{\\s*name\\s+type\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}" + } + ] } ] } diff --git a/lib/filters/index.js b/lib/filters/index.js index 9bd60b99b..f385994cb 100644 --- a/lib/filters/index.js +++ b/lib/filters/index.js @@ -36,7 +36,8 @@ module.exports = ruleSource => { method = (method || 'get').toLowerCase(); valid = valid || []; - const bodyFilters = valid.filter(v => !!v.path); + const bodyFilters = valid.filter(v => !!v.path && !v.regex); + const bodyRegexFilters = valid.filter(v => !!v.path && !!v.regex); const queryFilters = valid.filter(v => !!v.queryParam); // now track if there's any values that we need to interpolate later @@ -82,11 +83,13 @@ module.exports = ruleSource => { } // if validity filters are present, at least one must be satisfied - if (bodyFilters.length || queryFilters.length) { + if (bodyFilters.length || bodyRegexFilters.length || + queryFilters.length) { let isValid; + let parsedBody; if (bodyFilters.length) { - const parsedBody = tryJSONParse(req.body); + parsedBody = tryJSONParse(req.body); // validate against the body isValid = bodyFilters.some(({ path, value }) => { @@ -94,6 +97,22 @@ module.exports = ruleSource => { }); } + if (!isValid && bodyRegexFilters.length) { + parsedBody = parsedBody || tryJSONParse(req.body); + + // validate against the body by regex + isValid = bodyRegexFilters.some(({ path, regex }) => { + try { + const re = new RegExp(regex); + return re.test(undefsafe(parsedBody, path)); + } catch (err) { + logger.error({err, path, regex}, + 'failed to test regex rule'); + return false; + } + }); + } + // no need to check query filters if the request is already valid if (!isValid && queryFilters.length) { const parsedQuerystring = qs.parse(querystring); diff --git a/test/fixtures/relay.json b/test/fixtures/relay.json index af394a7ad..6ade1c050 100644 --- a/test/fixtures/relay.json +++ b/test/fixtures/relay.json @@ -63,5 +63,22 @@ "value": ".snyk" } ] + }, + { + "//": "used to filter only the wanted graphql query", + "method": "POST", + "path": "/graphql", + "valid": [ + { + "//": "malformed regex does not break other filters", + "path": "query", + "regex": "INVALID regex (" + }, + { + "//": "query for all the file names 2 levels deep in the repo. used for auto project detection", + "path": "query", + "regex": "{\\s*repositoryOwner\\(login:\\s*\"([a-zA-Z0-9-_.]+)\"\\)\\s*{\\s*repository\\(name:\\s*\"([a-zA-Z0-9-_.]+)\"\\)\\s*{\\s*object\\(expression:\\s*\"([^\"{}]+)\"\\)\\s*{\\s*\\.\\.\\.\\s*on\\s+Tree\\s+{\\s*entries\\s*{\\s*name\\s+type\\s+object\\s*{\\s*\\.\\.\\.\\s*on\\s+Tree\\s+{\\s*entries\\s*{\\s*name\\s+type\\s+object\\s*{\\s*\\.\\.\\.on\\s+Tree\\s*{\\s*entries\\s*{\\s*name\\s+type\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}\\s*}" + } + ] } ] diff --git a/test/unit/filters.test.js b/test/unit/filters.test.js index b63b7b69a..07f040bd2 100644 --- a/test/unit/filters.test.js +++ b/test/unit/filters.test.js @@ -6,7 +6,7 @@ const jsonBuffer = (body) => Buffer.from(JSON.stringify(body)); test('filter on body', t => { const filter = Filters(require(__dirname + '/../fixtures/relay.json')); - t.plan(9); + t.plan(14); t.pass('filters loaded'); filter({ @@ -71,6 +71,87 @@ test('filter on body', t => { t.equal(res, undefined, 'no follow allowed'); }); + filter({ + url: '/graphql', + method: 'POST', + body: jsonBuffer({ + query: `{ + repositoryOwner(login: "_REPO_OWNER_") { + repository(name: "_REPO_NAME_") { + object(expression: "_BRANCH_/_NAME_") { + ... on Tree { + entries { + name + type + object { + ... on Tree { + entries { + name + type + object { + ...on Tree { + entries { + name + type + } + } + } + } + } + } + } + } + } + } + } + }`, + }) + }, (error, res) => { + t.equal(error, null, 'no error'); + t.equal(res, '/graphql', 'allows the path request'); + }); + + filter({ + url: '/graphql', + method: 'POST', + body: jsonBuffer({ + // "NoSQL injection" + query: `{ + repositoryOwner(login: "search: "{\"username\": {\"$regex\": \"sue\"}, \"email\": {\"$regex\": \"sue\"}}"") { + repository(name: "_REPO_NAME_") { + object(expression: "_BRANCH_/_NAME_") { + ... on Tree { + entries { + name + type + object { + ... on Tree { + entries { + name + type + object { + ...on Tree { + entries { + name + type + } + } + } + } + } + } + } + } + } + } + } + }`, + }) + }, (error, res) => { + t.ok(error, 'got an error'); + t.equal(error.message, 'blocked', 'has been blocked'); + t.equal(res, undefined, 'no follow allowed'); + }); }); test('filter on querystring', t => {