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

Commit a802109

Browse files
committed
Add validation for doc ID and attachment names
1 parent 9594580 commit a802109

File tree

4 files changed

+1313
-20
lines changed

4 files changed

+1313
-20
lines changed

CHANGES.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
- [FIXED] Hang caused by plugins (i.e. retry plugin) preventing callback execution
33
by attempting to retry on errors received after starting to return the response body.
44
- [DEPRECATED] This library is now deprecated and will be EOL on Dec 31 2021.
5+
- [IMPROVED] - Document IDs and attachment names are now rejected if they could cause an unexpected
6+
Cloudant request. We have seen that some applications pass unsantized document IDs to SDK functions
7+
(e.g. direct from user requests). In response to this we have updated many functions to reject
8+
obviously invalid paths. However, for complete safety applications must still validate that
9+
document IDs and attachment names match expected patterns.
510

611
# 4.4.0 (2021-06-18)
712
- [FIXED] Parsing of max-age from Set-Cookie headers.

Jenkinsfile

+37-19
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
def getEnvForSuite(suiteName) {
1717
// Base environment variables
1818
def envVars = [
19-
"NVM_DIR=${env.HOME}/.nvm",
20-
"MOCHA_TIMEOUT=60000" // 60s
19+
"NVM_DIR=${env.HOME}/.nvm"
2120
]
2221

2322
// Add test suite specific environment variables
@@ -27,13 +26,34 @@ def getEnvForSuite(suiteName) {
2726
envVars.add("SERVER_URL=${env.SDKS_TEST_SERVER_URL}")
2827
envVars.add("cloudant_iam_token_server=${env.SDKS_TEST_IAM_SERVER}")
2928
break
29+
case 'nock-test':
30+
break
3031
default:
3132
error("Unknown test suite environment ${suiteName}")
3233
}
3334

3435
return envVars
3536
}
3637

38+
def installAndTest(version, testSuite) {
39+
try {
40+
// Actions:
41+
// 1. Load NVM
42+
// 2. Install/use required Node.js version
43+
// 3. Install mocha-jenkins-reporter so that we can get junit style output
44+
// 4. Run tests
45+
sh """
46+
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
47+
nvm install ${version}
48+
nvm use ${version}
49+
npm install mocha-jenkins-reporter --save-dev
50+
./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
51+
"""
52+
} finally {
53+
junit '**/test-results.xml'
54+
}
55+
}
56+
3757
def setupNodeAndTest(version, testSuite='test') {
3858
node {
3959
// Install NVM
@@ -42,25 +62,16 @@ def setupNodeAndTest(version, testSuite='test') {
4262
unstash name: 'built'
4363

4464
// Run tests using creds
45-
withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'cloudant_username', passwordVariable: 'cloudant_password'), string(credentialsId: 'testServerIamApiKey', variable: 'cloudant_iam_api_key')]) {
46-
withEnv(getEnvForSuite("${testSuite}")) {
47-
try {
48-
// Actions:
49-
// 1. Load NVM
50-
// 2. Install/use required Node.js version
51-
// 3. Install mocha-jenkins-reporter so that we can get junit style output
52-
// 4. Run tests
53-
sh """
54-
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
55-
nvm install ${version}
56-
nvm use ${version}
57-
npm install mocha-jenkins-reporter --save-dev
58-
./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
59-
"""
60-
} finally {
61-
junit '**/test-results.xml'
65+
if(testSuite == 'test') {
66+
withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'cloudant_username', passwordVariable: 'cloudant_password'), string(credentialsId: 'testServerIamApiKey', variable: 'cloudant_iam_api_key')]) {
67+
withEnv(getEnvForSuite("${testSuite}")) {
68+
installAndTest(version, testSuite)
6269
}
6370
}
71+
} else {
72+
withEnv(getEnvForSuite("${testSuite}")) {
73+
installAndTest(version, testSuite)
74+
}
6475
}
6576
}
6677
}
@@ -80,6 +91,9 @@ stage('QA') {
8091
//12.x LTS
8192
setupNodeAndTest('lts/erbium')
8293
},
94+
Node12xWithNock : {
95+
setupNodeAndTest('lts/erbium', 'nock-test')
96+
},
8397
Node14x : {
8498
//14.x LTS
8599
setupNodeAndTest('lts/fermium')
@@ -88,6 +102,10 @@ stage('QA') {
88102
// Current
89103
setupNodeAndTest('node')
90104
},
105+
NodeWithNock : {
106+
// Current
107+
setupNodeAndTest('node', 'nock-test')
108+
},
91109
])
92110
}
93111

cloudant.js

+140-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2015, 2019 IBM Corp. All rights reserved.
1+
// Copyright © 2015, 2021 IBM Corp. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@ var nanodebug = require('debug')('nano');
2121

2222
const Client = require('./lib/client.js');
2323
const BasePlugin = require('./plugins/base.js');
24+
const INVALID_DOC_ID_MSG = 'Invalid document ID';
25+
const INVALID_ATT_MSG = 'Invalid attachment name';
2426

2527
Cloudant.BasePlugin = BasePlugin; // expose base plugin
2628

@@ -165,6 +167,136 @@ function Cloudant(options, callback) {
165167
body: query}, callback);
166168
};
167169

170+
// Encode '/' path separator if it exists within the document ID
171+
// or attachment name e.g. _design//foo will result in _design/%2Ffoo
172+
function encodePathSeparator(docName) {
173+
if (docName.includes('/')) {
174+
return docName.replace(/\//g, encodeURIComponent('/'));
175+
}
176+
return docName;
177+
}
178+
179+
// Validate document ID during document requests.
180+
// Raises an error if the ID is an `_` prefixed name
181+
// that isn't either `_design` or `_local`.
182+
function assertDocumentTypeId(docName) {
183+
if (docName && docName.startsWith('_')) {
184+
const possibleDocPrefixes = ['_local/', '_design/'];
185+
186+
for (let docPrefix of possibleDocPrefixes) {
187+
if (docName.startsWith(docPrefix) && docName !== docPrefix) {
188+
// encode '/' if it exists after the document prefix
189+
return docPrefix + encodePathSeparator(docName.slice(docPrefix.length));
190+
}
191+
}
192+
return new Error(`${INVALID_DOC_ID_MSG}: ${docName}`);
193+
}
194+
return docName;
195+
}
196+
197+
// Validate attachment name during attachment requests.
198+
// Raises an error if the name has a `_` prefixed name
199+
function assertValidAttachmentName(attName) {
200+
if (attName && attName.startsWith('_')) {
201+
const error = new Error(`${INVALID_ATT_MSG}: ${attName}`);
202+
return error;
203+
} else if (attName && attName.includes('/')) {
204+
// URI encode slashes in attachment name
205+
attName = encodePathSeparator(attName);
206+
return attName;
207+
}
208+
return attName;
209+
}
210+
211+
function callbackError(result, callback) {
212+
if (callback) {
213+
return callback(result, null);
214+
}
215+
return Promise.reject(result);
216+
}
217+
218+
var getDoc = function getDoc(docName, qs0, callback0) {
219+
const {opts, callback} = getCallback(qs0, callback0);
220+
var docResult = assertDocumentTypeId(docName);
221+
if (docResult instanceof Error) {
222+
return callbackError(docResult, callback);
223+
} else {
224+
return nano._use(db).get(docResult, opts, callback);
225+
}
226+
};
227+
228+
var headDoc = function headDoc(docName, callback0) {
229+
const {callback} = getCallback(callback0);
230+
var docResult = assertDocumentTypeId(docName);
231+
if (docResult instanceof Error) {
232+
return callbackError(docResult, callback);
233+
} else {
234+
return nano._use(db).head(docResult, callback);
235+
}
236+
};
237+
238+
var getAttachment = function getAttachment(docName, attachmentName, qs0, callback0) {
239+
const {opts, callback} = getCallback(qs0, callback0);
240+
var docResult = assertDocumentTypeId(docName);
241+
var attResult = assertValidAttachmentName(attachmentName);
242+
if (docResult instanceof Error) {
243+
return callbackError(docResult, callback);
244+
} else if (attResult instanceof Error) {
245+
return callbackError(attResult, callback);
246+
} else {
247+
return nano._use(db).attachment.get(docResult, attResult, opts, callback);
248+
}
249+
};
250+
251+
var deleteDoc = function deleteDoc(docName, qs0, callback0) {
252+
const {opts, callback} = getCallback(qs0, callback0);
253+
var docResult = assertDocumentTypeId(docName);
254+
if (docResult instanceof Error) {
255+
return callbackError(docResult, callback);
256+
} else {
257+
return nano._use(db).destroy(docResult, opts, callback);
258+
}
259+
};
260+
261+
var deleteAttachment = function deleteAttachment(docName, attachmentName, qs0, callback0) {
262+
const {opts, callback} = getCallback(qs0, callback0);
263+
var docResult = assertDocumentTypeId(docName);
264+
var attResult = assertValidAttachmentName(attachmentName);
265+
if (docResult instanceof Error) {
266+
return callbackError(docResult, callback);
267+
} else if (attResult instanceof Error) {
268+
return callbackError(attResult, callback);
269+
} else {
270+
return nano._use(db).attachment.destroy(docResult, attResult, opts, callback);
271+
}
272+
};
273+
274+
var putAttachment = function putAttachment(docName, attachmentName, att, contentType, qs0, callback0) {
275+
const {opts, callback} = getCallback(qs0, callback0);
276+
var docResult = assertDocumentTypeId(docName);
277+
var attResult = assertValidAttachmentName(attachmentName);
278+
if (docResult instanceof Error) {
279+
return callbackError(docResult, callback);
280+
} else if (attResult instanceof Error) {
281+
return callbackError(attResult, callback);
282+
} else {
283+
return nano._use(db).attachment.insert(docResult, attResult, att, contentType, opts, callback);
284+
}
285+
};
286+
287+
var putDoc = function putDoc(docBody, qs0, callback0) {
288+
const {opts, callback} = getCallback(qs0, callback0);
289+
if (typeof opts === 'string') {
290+
var docResult = assertDocumentTypeId(opts);
291+
if (docResult instanceof Error) {
292+
return callbackError(docResult, callback);
293+
} else {
294+
return nano._use(db).insert(docBody, docResult, callback);
295+
}
296+
}
297+
return nano._use(db).insert(docBody, opts, callback);
298+
};
299+
168300
// Partitioned Databases
169301
// ---------------------
170302

@@ -247,6 +379,13 @@ function Cloudant(options, callback) {
247379
obj.index = index;
248380
obj.index.del = index_del; // eslint-disable-line camelcase
249381
obj.find = find;
382+
obj.destroy = deleteDoc;
383+
obj.get = getDoc;
384+
obj.head = headDoc;
385+
obj.insert = putDoc;
386+
obj.attachment.destroy = deleteAttachment;
387+
obj.attachment.get = getAttachment;
388+
obj.attachment.insert = putAttachment;
250389

251390
obj.partitionInfo = partitionInfo;
252391
obj.partitionedFind = partitionedFind;

0 commit comments

Comments
 (0)