Skip to content
This repository has been archived by the owner on Mar 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #460 from cloudant/validation-feature
Browse files Browse the repository at this point in the history
Add validation for doc ID and attachment names
  • Loading branch information
ricellis authored Aug 26, 2021
2 parents 9594580 + a802109 commit 8417bf5
Show file tree
Hide file tree
Showing 4 changed files with 1,313 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
- [FIXED] Hang caused by plugins (i.e. retry plugin) preventing callback execution
by attempting to retry on errors received after starting to return the response body.
- [DEPRECATED] This library is now deprecated and will be EOL on Dec 31 2021.
- [IMPROVED] - Document IDs and attachment names are now rejected if they could cause an unexpected
Cloudant request. We have seen that some applications pass unsantized document IDs to SDK functions
(e.g. direct from user requests). In response to this we have updated many functions to reject
obviously invalid paths. However, for complete safety applications must still validate that
document IDs and attachment names match expected patterns.

# 4.4.0 (2021-06-18)
- [FIXED] Parsing of max-age from Set-Cookie headers.
Expand Down
56 changes: 37 additions & 19 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
def getEnvForSuite(suiteName) {
// Base environment variables
def envVars = [
"NVM_DIR=${env.HOME}/.nvm",
"MOCHA_TIMEOUT=60000" // 60s
"NVM_DIR=${env.HOME}/.nvm"
]

// Add test suite specific environment variables
Expand All @@ -27,13 +26,34 @@ def getEnvForSuite(suiteName) {
envVars.add("SERVER_URL=${env.SDKS_TEST_SERVER_URL}")
envVars.add("cloudant_iam_token_server=${env.SDKS_TEST_IAM_SERVER}")
break
case 'nock-test':
break
default:
error("Unknown test suite environment ${suiteName}")
}

return envVars
}

def installAndTest(version, testSuite) {
try {
// Actions:
// 1. Load NVM
// 2. Install/use required Node.js version
// 3. Install mocha-jenkins-reporter so that we can get junit style output
// 4. Run tests
sh """
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install ${version}
nvm use ${version}
npm install mocha-jenkins-reporter --save-dev
./node_modules/mocha/bin/mocha --reporter mocha-jenkins-reporter --reporter-options junit_report_path=./${testSuite}/test-results.xml,junit_report_stack=true,junit_report_name=${testSuite} test --grep 'Virtual Hosts' --invert
"""
} finally {
junit '**/test-results.xml'
}
}

def setupNodeAndTest(version, testSuite='test') {
node {
// Install NVM
Expand All @@ -42,25 +62,16 @@ def setupNodeAndTest(version, testSuite='test') {
unstash name: 'built'

// Run tests using creds
withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'cloudant_username', passwordVariable: 'cloudant_password'), string(credentialsId: 'testServerIamApiKey', variable: 'cloudant_iam_api_key')]) {
withEnv(getEnvForSuite("${testSuite}")) {
try {
// Actions:
// 1. Load NVM
// 2. Install/use required Node.js version
// 3. Install mocha-jenkins-reporter so that we can get junit style output
// 4. Run tests
sh """
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install ${version}
nvm use ${version}
npm install mocha-jenkins-reporter --save-dev
./node_modules/mocha/bin/mocha --timeout $MOCHA_TIMEOUT --reporter mocha-jenkins-reporter --reporter-options junit_report_path=./${testSuite}/test-results.xml,junit_report_stack=true,junit_report_name=${testSuite} ${testSuite} --grep 'Virtual Hosts' --invert
"""
} finally {
junit '**/test-results.xml'
if(testSuite == 'test') {
withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'cloudant_username', passwordVariable: 'cloudant_password'), string(credentialsId: 'testServerIamApiKey', variable: 'cloudant_iam_api_key')]) {
withEnv(getEnvForSuite("${testSuite}")) {
installAndTest(version, testSuite)
}
}
} else {
withEnv(getEnvForSuite("${testSuite}")) {
installAndTest(version, testSuite)
}
}
}
}
Expand All @@ -80,6 +91,9 @@ stage('QA') {
//12.x LTS
setupNodeAndTest('lts/erbium')
},
Node12xWithNock : {
setupNodeAndTest('lts/erbium', 'nock-test')
},
Node14x : {
//14.x LTS
setupNodeAndTest('lts/fermium')
Expand All @@ -88,6 +102,10 @@ stage('QA') {
// Current
setupNodeAndTest('node')
},
NodeWithNock : {
// Current
setupNodeAndTest('node', 'nock-test')
},
])
}

Expand Down
141 changes: 140 additions & 1 deletion cloudant.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2015, 2019 IBM Corp. All rights reserved.
// Copyright © 2015, 2021 IBM Corp. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,8 @@ var nanodebug = require('debug')('nano');

const Client = require('./lib/client.js');
const BasePlugin = require('./plugins/base.js');
const INVALID_DOC_ID_MSG = 'Invalid document ID';
const INVALID_ATT_MSG = 'Invalid attachment name';

Cloudant.BasePlugin = BasePlugin; // expose base plugin

Expand Down Expand Up @@ -165,6 +167,136 @@ function Cloudant(options, callback) {
body: query}, callback);
};

// Encode '/' path separator if it exists within the document ID
// or attachment name e.g. _design//foo will result in _design/%2Ffoo
function encodePathSeparator(docName) {
if (docName.includes('/')) {
return docName.replace(/\//g, encodeURIComponent('/'));
}
return docName;
}

// Validate document ID during document requests.
// Raises an error if the ID is an `_` prefixed name
// that isn't either `_design` or `_local`.
function assertDocumentTypeId(docName) {
if (docName && docName.startsWith('_')) {
const possibleDocPrefixes = ['_local/', '_design/'];

for (let docPrefix of possibleDocPrefixes) {
if (docName.startsWith(docPrefix) && docName !== docPrefix) {
// encode '/' if it exists after the document prefix
return docPrefix + encodePathSeparator(docName.slice(docPrefix.length));
}
}
return new Error(`${INVALID_DOC_ID_MSG}: ${docName}`);
}
return docName;
}

// Validate attachment name during attachment requests.
// Raises an error if the name has a `_` prefixed name
function assertValidAttachmentName(attName) {
if (attName && attName.startsWith('_')) {
const error = new Error(`${INVALID_ATT_MSG}: ${attName}`);
return error;
} else if (attName && attName.includes('/')) {
// URI encode slashes in attachment name
attName = encodePathSeparator(attName);
return attName;
}
return attName;
}

function callbackError(result, callback) {
if (callback) {
return callback(result, null);
}
return Promise.reject(result);
}

var getDoc = function getDoc(docName, qs0, callback0) {
const {opts, callback} = getCallback(qs0, callback0);
var docResult = assertDocumentTypeId(docName);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else {
return nano._use(db).get(docResult, opts, callback);
}
};

var headDoc = function headDoc(docName, callback0) {
const {callback} = getCallback(callback0);
var docResult = assertDocumentTypeId(docName);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else {
return nano._use(db).head(docResult, callback);
}
};

var getAttachment = function getAttachment(docName, attachmentName, qs0, callback0) {
const {opts, callback} = getCallback(qs0, callback0);
var docResult = assertDocumentTypeId(docName);
var attResult = assertValidAttachmentName(attachmentName);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else if (attResult instanceof Error) {
return callbackError(attResult, callback);
} else {
return nano._use(db).attachment.get(docResult, attResult, opts, callback);
}
};

var deleteDoc = function deleteDoc(docName, qs0, callback0) {
const {opts, callback} = getCallback(qs0, callback0);
var docResult = assertDocumentTypeId(docName);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else {
return nano._use(db).destroy(docResult, opts, callback);
}
};

var deleteAttachment = function deleteAttachment(docName, attachmentName, qs0, callback0) {
const {opts, callback} = getCallback(qs0, callback0);
var docResult = assertDocumentTypeId(docName);
var attResult = assertValidAttachmentName(attachmentName);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else if (attResult instanceof Error) {
return callbackError(attResult, callback);
} else {
return nano._use(db).attachment.destroy(docResult, attResult, opts, callback);
}
};

var putAttachment = function putAttachment(docName, attachmentName, att, contentType, qs0, callback0) {
const {opts, callback} = getCallback(qs0, callback0);
var docResult = assertDocumentTypeId(docName);
var attResult = assertValidAttachmentName(attachmentName);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else if (attResult instanceof Error) {
return callbackError(attResult, callback);
} else {
return nano._use(db).attachment.insert(docResult, attResult, att, contentType, opts, callback);
}
};

var putDoc = function putDoc(docBody, qs0, callback0) {
const {opts, callback} = getCallback(qs0, callback0);
if (typeof opts === 'string') {
var docResult = assertDocumentTypeId(opts);
if (docResult instanceof Error) {
return callbackError(docResult, callback);
} else {
return nano._use(db).insert(docBody, docResult, callback);
}
}
return nano._use(db).insert(docBody, opts, callback);
};

// Partitioned Databases
// ---------------------

Expand Down Expand Up @@ -247,6 +379,13 @@ function Cloudant(options, callback) {
obj.index = index;
obj.index.del = index_del; // eslint-disable-line camelcase
obj.find = find;
obj.destroy = deleteDoc;
obj.get = getDoc;
obj.head = headDoc;
obj.insert = putDoc;
obj.attachment.destroy = deleteAttachment;
obj.attachment.get = getAttachment;
obj.attachment.insert = putAttachment;

obj.partitionInfo = partitionInfo;
obj.partitionedFind = partitionedFind;
Expand Down
Loading

0 comments on commit 8417bf5

Please sign in to comment.