Socket
Socket
Sign inDemoInstall

vuln-regex-detector

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

vuln-regex-detector - npm Package Compare versions

Comparing version 1.0.1 to 1.1.0

22

CHANGELOG.md
# v1
## v1.1
### v1.1.0
*Additions*
- Add a synchronous API for use in an eslint plugin.
- Add an in-memory cache in case the same regex is repeated during a Node process's lifetime.
Contributors:
- [Jamie Davis](davisjam@vt.edu)
## v1.0
### v1.0.1
*Additions*
- Misc. bugfixes
- Enhance test suites
Contributors:
- [Jamie Davis](davisjam@vt.edu)
### v1.0.0

@@ -6,0 +28,0 @@

5

package.json
{
"name": "vuln-regex-detector",
"version": "1.0.1",
"version": "1.1.0",
"description": "Detect vulnerable regexes by querying a service hosted at Virginia Tech.",

@@ -10,5 +10,6 @@ "main": "vuln-regex-detector-client.js",

"dependencies": {
"sync-request": "^6.0.0"
},
"scripts": {
"test": "mocha",
"test": "npm run lint && mocha",
"lint": "eslint vuln-regex-detector-client.js test/*"

@@ -15,0 +16,0 @@ },

27

README.md

@@ -29,3 +29,3 @@ # Summary

The module exports:
- a function `test`
- functions `test` and `testSync`
- a set of responses `responses`

@@ -41,3 +41,3 @@

*
* returns a Promise fulfilled with a response or rejected with RESPONSE_INVALID or an error.
* returns a Promise fulfilled with a vulnerable/safe/unknown or rejected with invalid.
*/

@@ -47,5 +47,20 @@ vulnRegexDetector.test (regex, config)

## testSync
```javascript
/**
* @regex: RegExp or string (e.g. /re/ or 're')
* @config: object with fields: hostname port
* default: 'toybox.cs.vt.edu', '8000'
*
* returns with vulnerable/safe/unknown/invalid.
*/
vulnRegexDetector.testSync (regex, config)
```
NB: This API makes synchronous HTTP queries, which can be slow. You should probably not use it.
## responses
If fulfilled, the returned Promise takes on one of the following values:
If fulfilled, the returned Promise gets one of the following values:
- `responses.vulnerable`

@@ -55,3 +70,3 @@ - `responses.safe`

If rejected, the returned Promise might be:
If rejected, the returned Promise gets the value:
- `responses.invalid`

@@ -64,5 +79,7 @@

If the regex has not been seen before, the server will respond "UNKNOWN" and test it in the background.
If the regex has not been seen before, the server will respond "unknown" and test it in the background.
The server cannot test synchronously because testing is expensive (potentially minutes) and there might be a long line.
If the server has not seen the regex before, it should have an answer if you query it again in a few minutes.
## Privacy

@@ -69,0 +86,0 @@

@@ -10,3 +10,2 @@ /* eslint-env node, mocha */

vulnRegexDetector.responses.unknown;
console.log(`outcome: ${outcome} isOK ${isOK}`);
assert.ok(isOK, `outcome ${outcome} should be safe|unknown`);

@@ -19,3 +18,2 @@ }

outcome === vulnRegexDetector.responses.unknown;
console.log(`outcome: ${outcome} isOK ${isOK}`);
assert.ok(isOK, `outcome ${outcome} should be vulnerable|unknown`);

@@ -29,3 +27,2 @@ }

outcome === vulnRegexDetector.responses.unknown;
console.log(`outcome: ${outcome} isOK ${isOK}`);
assert.ok(isOK, `outcome ${outcome} should be vulnerable|safe|unknown`);

@@ -37,3 +34,2 @@ }

const isOK = outcome === vulnRegexDetector.responses.invalid;
console.log(`outcome: ${outcome} isOK ${isOK}`);
assert.ok(isOK, `outcome ${outcome} should be invalid`);

@@ -43,55 +39,161 @@ }

describe('vulnRegexDetector', () => {
describe('input format', () => {
it('should accept regexes as strings', () => {
return vulnRegexDetector.test('abc')
.then(assertIsOK, assertIsOK);
describe('checkRegex', () => {
describe('input format', () => {
it('should accept regexes as strings', () => {
return vulnRegexDetector.test('abc')
.then(assertIsOK, assertIsOK);
});
it('should accept regexes as RegExps', () => {
return vulnRegexDetector.test(/abc/)
.then(assertIsOK, assertIsOK);
});
it('should reject an undefined regex', () => {
return vulnRegexDetector.test(undefined)
.then(assertIsInvalid, assertIsInvalid);
});
it('should reject a random object', () => {
return vulnRegexDetector.test({foo: 1})
.then(assertIsInvalid, assertIsInvalid);
});
it('should accept config', () => {
return vulnRegexDetector.test('abc', { hostname: 'toybox.cs.vt.edu', port: 8000 })
.then(assertIsOK, assertIsOK);
});
});
it('should accept regexes as RegExps', () => {
return vulnRegexDetector.test(/abc/)
.then(assertIsOK, assertIsOK);
describe('outcome validity', () => {
it('should label safe as such: simple', () => {
return vulnRegexDetector.test('abc')
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown);
});
it('should label safe as such: non-vulnerable star height', () => {
return vulnRegexDetector.test('(ab+)+$')
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown);
});
it('should label vulnerable as such: star height', () => {
return vulnRegexDetector.test('(a+)+$')
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown);
});
it('should label vulnerable as such: QOD', () => {
return vulnRegexDetector.test(/(\d|\w)+$/)
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown);
});
it('should label vulnerable as such: QOA', () => {
return vulnRegexDetector.test(/.*a.*a.*a.*a$/)
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown);
});
});
it('should reject an undefined regex', () => {
return vulnRegexDetector.test(undefined)
.then(assertIsInvalid, assertIsInvalid);
describe('invalid config', () => {
it('should reject an invalid host', () => {
return vulnRegexDetector.test('abcde', { hostname: 'no such host', port: 8000 })
.then((response) => {
assert.ok(false, `Invalid config should not have resolved (with ${response})`);
}, (err) => {
assert.ok(err === vulnRegexDetector.responses.invalid, `Invalid config rejected, but with ${err}`);
});
});
it('should reject an invalid port', () => {
return vulnRegexDetector.test('abcde', { hostname: 'toybox.cs.vt.edu', port: 22 })
.then((response) => {
assert.ok(false, `Invalid config should not have resolved (with ${response})`);
}, (err) => {
assert.ok(err === vulnRegexDetector.responses.invalid, `Invalid config rejected, but with ${err}`);
});
});
});
});
it('should reject a random object', () => {
return vulnRegexDetector.test({foo: 1})
.then(assertIsInvalid, assertIsInvalid);
describe('checkRegexSync', () => {
describe('input format', () => {
it('should accept regexes as strings', () => {
assertIsOK(vulnRegexDetector.testSync('abc'));
});
it('should accept regexes as RegExps', () => {
assertIsOK(vulnRegexDetector.testSync(/abc/));
});
it('should reject an undefined regex', () => {
assertIsInvalid(vulnRegexDetector.testSync(undefined));
});
it('should reject a random object', () => {
assertIsInvalid(vulnRegexDetector.testSync({foo: 1}));
});
it('should accept config', () => {
assertIsOK(vulnRegexDetector.testSync('abc', { hostname: 'toybox.cs.vt.edu', port: 8000 }));
});
});
it('should accept config', () => {
return vulnRegexDetector.test('abc', { hostname: 'toybox.cs.vt.edu', port: 8000 })
.then(assertIsOK, assertIsOK);
describe('outcome validity', () => {
it('should label safe as such: simple', () => {
assertIsSafeOrUnknown(vulnRegexDetector.testSync('abc'));
});
it('should label safe as such: non-vulnerable star height', () => {
assertIsSafeOrUnknown(vulnRegexDetector.testSync('(ab+)+$'));
});
it('should label vulnerable as such: star height', () => {
assertIsVulnerableOrUnknown(vulnRegexDetector.testSync('(a+)+$'));
});
it('should label vulnerable as such: QOD', () => {
assertIsVulnerableOrUnknown(vulnRegexDetector.testSync(/(\d|\w)+$/));
});
it('should label vulnerable as such: QOA', () => {
assertIsVulnerableOrUnknown(vulnRegexDetector.testSync(/.*a.*a.*a.*a$/));
});
});
});
describe('outcome validity', () => {
it('should label safe as such: simple', () => {
return vulnRegexDetector.test('abc')
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown);
describe('invalid config', () => {
it('should reject an invalid host', () => {
const response = vulnRegexDetector.testSync('abcde', { hostname: 'no such host', port: 8000 });
assert.ok(response === vulnRegexDetector.responses.invalid, `Invalid config returned ${response}`);
});
it('should reject an invalid port', () => {
const response = vulnRegexDetector.testSync('abcde', { hostname: 'toybox.cs.vt.edu', port: 22 });
assert.ok(response === vulnRegexDetector.responses.invalid, `Invalid config returned ${response}`);
});
});
it('should label safe as such: non-vulnerable star height', () => {
return vulnRegexDetector.test('(ab+)+$')
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown);
describe('cache', () => {
it('should hit cache on successive duplicate queries', () => {
for (let i = 0; i < 10; i++) {
assertIsSafeOrUnknown(vulnRegexDetector.testSync('abc'));
}
});
});
});
it('should label vulnerable as such: star height', () => {
return vulnRegexDetector.test('(a+)+$')
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown);
describe('responses', () => {
it('has vulnerable', () => {
return assert.ok(vulnRegexDetector.responses.vulnerable, 'Missing vulnerable');
});
it('should label vulnerable as such: QOD', () => {
return vulnRegexDetector.test(/(\d|\w)+$/)
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown);
it('has safe', () => {
return assert.ok(vulnRegexDetector.responses.safe, 'Missing safe');
});
it('should label vulnerable as such: QOA', () => {
return vulnRegexDetector.test(/.*a.*a.*a.*a$/)
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown);
it('has unknown', () => {
return assert.ok(vulnRegexDetector.responses.unknown, 'Missing unknown');
});
it('has invalid', () => {
return assert.ok(vulnRegexDetector.responses.invalid, 'Missing invalid');
});
});
});

@@ -5,2 +5,3 @@ 'use strict';

const https = require('https');
const syncRequest = require('sync-request');

@@ -20,10 +21,15 @@ /* Globals. */

const LOGGING = true;
const LOGGING = false;
const USE_CACHE = true;
/* Map pattern to RESPONSE_VULNERABLE or RESPONSE_SAFE in case of duplicate queries.
* We do not cache RESPONSE_UNKNOWN or RESPONSE_INVALID responses since these might change. */
let patternCache = {};
/**
* @regex: RegExp or string (e.g. /re/ or 're')
* @config: object with fields: hostname port
* @param regex: RegExp or string (e.g. /re/ or 're')
* @param config: object with fields: hostname port
* default: 'toybox.cs.vt.edu', '8000'
*
* returns a Promise fulfilled with a response or rejected with RESPONSE_INVALID or an error.
* returns a Promise fulfilled with a response or rejected with RESPONSE_INVALID.
*/

@@ -34,4 +40,147 @@ function checkRegex (regex, config) {

/* Validate args. */
// regex
/* Handle args. */
try {
[_pattern, _config] = handleArgs(regex, config);
} catch (e) {
return Promise.reject(RESPONSE_INVALID);
}
log(`Input OK. _pattern /${_pattern}/ _config ${JSON.stringify(_config)}`);
let postObject = generatePostObject(_pattern);
let postBuffer = JSON.stringify(postObject);
let postHeaders = generatePostHeaders(_config, Buffer.byteLength(postBuffer));
// Wrapper so we can return a Promise.
function promiseResult (options, data) {
log(`promiseResult: data ${data}`);
return new Promise((resolve, reject) => {
if (USE_CACHE) {
/* Check cache to avoid I/O. */
const cacheHit = checkCache(_pattern);
if (cacheHit !== RESPONSE_UNKNOWN) {
log(`Cache hit: ${cacheHit}`);
return resolve(cacheHit);
}
}
const req = https.request(options, (res) => {
res.setEncoding('utf8');
let response = '';
res.on('data', (chunk) => {
log(`Got data`);
response += chunk;
});
res.on('end', () => {
log(`end: I got ${JSON.stringify(response)}`);
const result = serverResponseToRESPONSE(response);
log(`end: result ${result}`);
if (USE_CACHE) {
updateCache(postObject.pattern, result);
}
if (result === RESPONSE_INVALID) {
return reject(result);
} else {
return resolve(result);
}
});
});
req.on('error', (e) => {
log(`Error: ${e}`);
return reject(RESPONSE_INVALID);
});
// Write data to request body.
log(`Writing to req:\n${data}`);
req.write(data);
req.end();
});
}
return promiseResult(postHeaders, postBuffer);
}
/**
* @param regex: RegExp or string (e.g. /re/ or 're')
* @param config: object with fields: hostname port
* default: 'toybox.cs.vt.edu', '8000'
*
* returns synchronous result: RESPONSE_X
*
* Since this makes a synchronous HTTP query it will be slow.
*/
function checkRegexSync (regex, config) {
let _pattern;
let _config;
/* Handle args. */
try {
[_pattern, _config] = handleArgs(regex, config);
} catch (e) {
return RESPONSE_INVALID;
}
log(`Input OK. _pattern /${_pattern}/ _config ${JSON.stringify(_config)}`);
if (USE_CACHE) {
/* Check cache to avoid I/O. */
const cacheHit = checkCache(_pattern);
if (cacheHit !== RESPONSE_UNKNOWN) {
log(`Cache hit: ${cacheHit}`);
return cacheHit;
}
}
let postObject = generatePostObject(_pattern);
let postBuffer = JSON.stringify(postObject);
let postHeaders = generatePostHeaders(_config, Buffer.byteLength(postBuffer));
let url = `https://${postHeaders.hostname}:${postHeaders.port}${postHeaders.path}`;
try {
log(`sending syncRequest: method ${postHeaders.method} url ${url} headers ${JSON.stringify(postHeaders.headers)} body ${postBuffer}`);
/* Send request. */
const response = syncRequest(postHeaders.method, url, {
headers: postHeaders.headers,
body: postBuffer
});
/* Extract body as JSON. */
let responseBody;
try {
responseBody = response.getBody('utf8');
} catch (e) {
log(`checkRegexSync: Unparseable response ${JSON.stringify(response)}`);
return RESPONSE_INVALID;
}
log(`checkRegexSync: I got ${responseBody}`);
/* Convert to a RESPONSE_X value. */
const result = serverResponseToRESPONSE(responseBody);
if (USE_CACHE) {
updateCache(postObject.pattern, result);
}
return result;
} catch (e) {
log(`syncRequest threw: ${JSON.stringify(e)}`);
return RESPONSE_INVALID;
}
}
/**********
* Helpers.
**********/
/**
* @param regex: Input to checkRegex, etc.
* @param config: Input to checkRegex, etc.
*
* Returns: [pattern, config] or throws exception
*/
function handleArgs (regex, config) {
let _pattern;
if (regex) {

@@ -52,6 +201,8 @@ if (typeof regex === 'string') {

if (!_pattern) {
return Promise.reject(RESPONSE_INVALID);
let errObj = { msg: 'Invalid args' };
throw errObj;
}
// config
let _config;
if (config && config.hasOwnProperty('hostname') && config.hasOwnProperty('port')) {

@@ -63,15 +214,21 @@ _config = config;

log(`Input OK. _pattern /${_pattern}/ _config ${JSON.stringify(_config)}`);
return [_pattern, _config];
}
// Prep POST request.
const postObj = {
pattern: _pattern,
/* Return object to be sent over the wire as JSON. */
function generatePostObject (pattern) {
const postObject = {
pattern: pattern,
language: 'javascript',
requestType: REQUEST_LOOKUP_ONLY
};
const postData = JSON.stringify(postObj);
const postOptions = {
hostname: _config.hostname,
port: _config.port,
return postObject;
}
/* Return headers for the POST request. */
function generatePostHeaders (config, payloadSize) {
const postHeaders = {
hostname: config.hostname,
port: config.port,
path: '/api/lookup',

@@ -81,53 +238,61 @@ method: 'POST',

'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
'Content-Length': payloadSize
}
};
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // TODO.
return postHeaders;
}
// Wrapper so we can return a Promise.
function promiseResult (options, data) {
log(`promiseResult: data ${JSON.stringify(data)}`);
return new Promise(function (resolve, reject) {
const req = https.request(options, (res) => {
log(`Hello in res`);
res.setEncoding('utf8');
/* response: raw response from server */
function serverResponseToRESPONSE (response) {
try {
const obj = JSON.parse(response);
if (obj.result === RESPONSE_UNKNOWN) {
return RESPONSE_UNKNOWN;
} else {
return obj.result.result;
}
} catch (e) {
return RESPONSE_INVALID;
}
}
let response = '';
res.on('data', (chunk) => {
log(`Got data`);
response += chunk;
});
/**********
* Cache.
**********/
res.on('end', () => {
const fullResponse = JSON.parse(response);
log(`end: I got ${JSON.stringify(fullResponse)}`);
function updateCache (pattern, response) {
if (!USE_CACHE) {
return;
}
let resolveWith;
if (fullResponse.result === RESPONSE_UNKNOWN) {
resolveWith = RESPONSE_UNKNOWN;
} else {
resolveWith = fullResponse.result.result;
}
log(`end: resolving with ${resolveWith}`);
resolve(resolveWith);
});
});
/* Only cache VULNERABLE|SAFE responses. */
if (response !== RESPONSE_VULNERABLE && response !== RESPONSE_SAFE) {
return;
}
req.on('error', (e) => {
log(`Error: ${e}`);
reject(e);
});
if (!patternCache.hasOwnProperty(pattern)) {
patternCache[pattern] = response;
}
}
// Write data to request body.
log(`Writing to req:\n${data}`);
req.write(data);
req.end();
});
/* Returns RESPONSE_{VULNERABLE|SAFE} on hit, else RESPONSE_UNKNOWN. */
function checkCache (pattern) {
if (!USE_CACHE) {
return RESPONSE_UNKNOWN;
}
return promiseResult(postOptions, postData);
const hit = patternCache[pattern];
if (hit) {
log(`checkCache: pattern ${pattern}: hit in patternCache\n ${JSON.stringify(patternCache)}`);
return hit;
} else {
return RESPONSE_UNKNOWN;
}
}
/* Helpers. */
/**********
* Utilities.
**********/
function log (msg) {

@@ -139,6 +304,9 @@ if (LOGGING) {

/* Public. */
/**********
* Exports.
**********/
module.exports = {
test: checkRegex,
testSync: checkRegexSync,
responses: {

@@ -145,0 +313,0 @@ vulnerable: RESPONSE_VULNERABLE,

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc