From 1d7b8ca7c1a2312f33258442eaf4c144bf923693 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 1 Aug 2017 01:38:36 -0700 Subject: [PATCH] http: better handling of api keys. --- lib/http/base.js | 50 ++++++++++++++++++++++------------------------ lib/http/server.js | 8 +++++--- lib/utils/util.js | 11 ++++++++++ lib/wallet/http.js | 35 +++++++++++++++++--------------- 4 files changed, 59 insertions(+), 45 deletions(-) diff --git a/lib/http/base.js b/lib/http/base.js index 5a0279f1..f81d6a67 100644 --- a/lib/http/base.js +++ b/lib/http/base.js @@ -181,27 +181,22 @@ HTTPBase.prototype.cors = function cors() { */ HTTPBase.prototype.basicAuth = function basicAuth(options) { + assert(options, 'Basic auth requires options.'); + let user = options.username; let pass = options.password; let realm = options.realm; - if (user) { - if (typeof user === 'string') { - assert(user.length <= 255, 'Username too long.'); - user = Buffer.from(user, 'utf8'); - } - assert(Buffer.isBuffer(user)); + if (user != null) { + assert(typeof user === 'string'); assert(user.length <= 255, 'Username too long.'); + assert(util.isAscii(user), 'Username must be ASCII.'); user = digest.hash256(user); } - if (typeof pass === 'string') { - assert(pass.length <= 255, 'Password too long.'); - pass = Buffer.from(pass, 'utf8'); - } - - assert(Buffer.isBuffer(pass)); + assert(typeof pass === 'string'); assert(pass.length <= 255, 'Password too long.'); + assert(util.isAscii(pass), 'Password must be ASCII.'); pass = digest.hash256(pass); if (!realm) @@ -209,11 +204,11 @@ HTTPBase.prototype.basicAuth = function basicAuth(options) { assert(typeof realm === 'string'); - function fail(res) { + const fail = (res) => { res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); res.setStatus(401); res.end(); - } + }; return async (req, res) => { const hdr = req.headers['authorization']; @@ -223,7 +218,7 @@ HTTPBase.prototype.basicAuth = function basicAuth(options) { return; } - if (hdr.length > 1000) { + if (hdr.length > 674) { fail(res); return; } @@ -235,12 +230,14 @@ HTTPBase.prototype.basicAuth = function basicAuth(options) { return; } - if (parts[0] !== 'Basic') { + const [type, b64] = parts; + + if (type !== 'Basic') { fail(res); return; } - const auth = Buffer.from(parts[1], 'base64').toString('utf8'); + const auth = Buffer.from(b64, 'base64').toString('ascii'); const items = auth.split(':'); const username = items.shift(); @@ -252,7 +249,7 @@ HTTPBase.prototype.basicAuth = function basicAuth(options) { return; } - const raw = Buffer.from(username, 'utf8'); + const raw = Buffer.from(username, 'ascii'); const hash = digest.hash256(raw); if (!ccmp(hash, user)) { @@ -266,7 +263,7 @@ HTTPBase.prototype.basicAuth = function basicAuth(options) { return; } - const raw = Buffer.from(password, 'utf8'); + const raw = Buffer.from(password, 'ascii'); const hash = digest.hash256(raw); if (!ccmp(hash, pass)) { @@ -316,16 +313,19 @@ HTTPBase.prototype.parseBody = async function parseBody(req, options) { if (req.method === 'GET') return body; - const data = await this.readBody(req, 'utf8', options); - - if (!data) - return body; - let type = req.contentType; if (options.contentType) type = options.contentType; + if (type === 'bin') + return body; + + const data = await this.readBody(req, 'utf8', options); + + if (!data) + return body; + switch (type) { case 'json': body = JSON.parse(data); @@ -333,8 +333,6 @@ HTTPBase.prototype.parseBody = async function parseBody(req, options) { case 'form': body = parsePairs(data, options.keyLimit); break; - default: - break; } return body; diff --git a/lib/http/server.js b/lib/http/server.js index 88d20394..c8f7b606 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -370,7 +370,7 @@ HTTPServer.prototype.handleSocket = function handleSocket(socket) { if (key.length > 255) throw new Error('Invalid API key.'); - const data = Buffer.from(key, 'utf8'); + const data = Buffer.from(key, 'ascii'); const hash = digest.hash256(data); if (!ccmp(hash, this.options.apiHash)) @@ -701,7 +701,7 @@ function HTTPOptions(options) { this.logger = null; this.node = null; this.apiKey = base58.encode(random.randomBytes(20)); - this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'utf8')); + this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); this.noAuth = false; this.prefix = null; @@ -742,8 +742,10 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { 'API key must be a string.'); assert(options.apiKey.length <= 255, 'API key must be under 256 bytes.'); + assert(util.isAscii(options.apiKey), + 'API key must be ascii.'); this.apiKey = options.apiKey; - this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'utf8')); + this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); } if (options.noAuth != null) { diff --git a/lib/utils/util.js b/lib/utils/util.js index 843046db..d4c00864 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -772,6 +772,17 @@ if (!''.startsWith) { }; } +/** + * Test whether a string is a plain + * ascii string (no control characters). + * @param {String} str + * @returns {Boolean} + */ + +util.isAscii = function isAscii(str) { + return /^[\t\n\r -~]*$/.test(str); +}; + /** * Get memory usage info. * @returns {Object} diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 471a5a79..8ee5ff7c 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -395,12 +395,11 @@ HTTPServer.prototype.initRouter = function initRouter() { for (const output of outputs) { const valid = new Validator([output]); + const raw = valid.buf('script'); + let script = null; - let script; - if (valid.has('script')) { - const raw = valid.buf('script'); + if (raw) script = Script.fromRaw(raw); - } options.outputs.push({ script: script, @@ -434,12 +433,11 @@ HTTPServer.prototype.initRouter = function initRouter() { for (const output of outputs) { const valid = new Validator([output]); + const raw = valid.buf('script'); + let script = null; - let script; - if (valid.has('script')) { - const raw = valid.buf('script'); + if (raw) script = Script.fromRaw(raw); - } options.outputs.push({ script: script, @@ -698,12 +696,13 @@ HTTPServer.prototype.initRouter = function initRouter() { const valid = req.valid(); const acct = valid.str('account'); const txs = await req.wallet.getHistory(acct); - const result = []; common.sortTX(txs); const details = await req.wallet.toDetails(txs); + const result = []; + for (const item of details) result.push(item.toJSON()); @@ -715,12 +714,13 @@ HTTPServer.prototype.initRouter = function initRouter() { const valid = req.valid(); const acct = valid.str('account'); const txs = await req.wallet.getPending(acct); - const result = []; common.sortTX(txs); const details = await req.wallet.toDetails(txs); + const result = []; + for (const item of details) result.push(item.toJSON()); @@ -731,7 +731,6 @@ HTTPServer.prototype.initRouter = function initRouter() { this.get('/:id/tx/range', async (req, res) => { const valid = req.valid(); const acct = valid.str('account'); - const result = []; const options = { start: valid.u32('start'), @@ -744,6 +743,8 @@ HTTPServer.prototype.initRouter = function initRouter() { const details = await req.wallet.toDetails(txs); + const result = []; + for (const item of details) result.push(item.toJSON()); @@ -852,7 +853,7 @@ HTTPServer.prototype.handleSocket = function handleSocket(socket) { if (!this.options.noAuth) { const valid = new Validator([args]); - const key = valid.str(0); + const key = valid.str(0, ''); if (key.length > 255) throw new Error('Invalid API key.'); @@ -943,7 +944,7 @@ function HTTPOptions(options) { this.logger = null; this.walletdb = null; this.apiKey = base58.encode(random.randomBytes(20)); - this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'utf8')); + this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); this.serviceHash = this.apiHash; this.noAuth = false; this.walletAuth = false; @@ -983,10 +984,12 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { if (options.apiKey != null) { assert(typeof options.apiKey === 'string', 'API key must be a string.'); - assert(options.apiKey.length <= 200, - 'API key must be under 200 bytes.'); + assert(options.apiKey.length <= 255, + 'API key must be under 255 bytes.'); + assert(util.isAscii(options.apiKey), + 'API key must be ASCII.'); this.apiKey = options.apiKey; - this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'utf8')); + this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); } if (options.noAuth != null) {