Skip to content

Commit 4e8446f

Browse files
authored
Fix acl and url escaping in auth token generation
1 parent b3540b2 commit 4e8446f

File tree

7 files changed

+50
-32
lines changed

7 files changed

+50
-32
lines changed

lib-es5/auth_token.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
"use strict";
1+
'use strict';
22

33
/**
44
* Authorization Token
55
* @module auth_token
66
*/
77

88
var crypto = require('crypto');
9+
var smart_escape = require('./utils/smart_escape').smart_escape;
10+
11+
var unsafe = /([ "#%&'/:;<=>?@[\]^`{|}~]+)/g;
912

1013
function digest(message, key) {
1114
return crypto.createHmac("sha256", Buffer.from(key, "hex")).update(message).digest('hex');
@@ -17,7 +20,8 @@ function digest(message, key) {
1720
* @return {string} escaped url
1821
*/
1922
function escapeToLower(url) {
20-
return encodeURIComponent(url).replace(/%../g, function (match) {
23+
var safeUrl = smart_escape(url, unsafe);
24+
return safeUrl.replace(/%../g, function (match) {
2125
return match.toLowerCase();
2226
});
2327
}
@@ -43,6 +47,7 @@ function escapeToLower(url) {
4347
*/
4448
module.exports = function (options) {
4549
var tokenName = options.token_name ? options.token_name : "__cld_token__";
50+
var tokenSeparator = "~";
4651
if (options.expiration == null) {
4752
if (options.duration != null) {
4853
var start = options.start_time != null ? options.start_time : Math.round(Date.now() / 1000);
@@ -63,11 +68,11 @@ module.exports = function (options) {
6368
tokenParts.push(`acl=${escapeToLower(options.acl)}`);
6469
}
6570
var toSign = [].concat(tokenParts);
66-
if (options.url) {
71+
if (options.url != null && options.acl == null) {
6772
var url = escapeToLower(options.url);
6873
toSign.push(`url=${url}`);
6974
}
70-
var auth = digest(toSign.join("~"), options.key);
75+
var auth = digest(toSign.join(tokenSeparator), options.key);
7176
tokenParts.push(`hmac=${auth}`);
72-
return `${tokenName}=${tokenParts.join('~')}`;
77+
return `${tokenName}=${tokenParts.join(tokenSeparator)}`;
7378
};

lib-es5/utils/index.js

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var isNumber = require("lodash/isNumber");
3535
var isObject = require("lodash/isObject");
3636
var isString = require("lodash/isString");
3737
var isUndefined = require("lodash/isUndefined");
38+
var smart_escape = require("./smart_escape").smart_escape;
3839

3940
var config = require("../config");
4041
var generate_token = require("../auth_token");
@@ -1001,18 +1002,6 @@ function unsigned_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, sec
10011002
}
10021003
return prefix;
10031004
}
1004-
// Based on CGI::unescape. In addition does not escape / :
1005-
// smart_escape = (string)->
1006-
// encodeURIComponent(string).replace(/%3A/g, ":").replace(/%2F/g, "/")
1007-
function smart_escape(string) {
1008-
var unsafe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : /([^a-zA-Z0-9_.\-\/:]+)/g;
1009-
1010-
return string.replace(unsafe, function (match) {
1011-
return match.split("").map(function (c) {
1012-
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
1013-
}).join("");
1014-
});
1015-
}
10161005

10171006
function api_url() {
10181007
var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'upload';

lib-es5/utils/smart_escape.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use strict";
2+
3+
// Based on CGI::unescape. In addition does not escape / :
4+
// smart_escape = (string)->
5+
// encodeURIComponent(string).replace(/%3A/g, ":").replace(/%2F/g, "/")
6+
function smart_escape(string) {
7+
var unsafe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : /([^a-zA-Z0-9_.\-\/:]+)/g;
8+
9+
return string.replace(unsafe, function (match) {
10+
return match.split("").map(function (c) {
11+
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
12+
}).join("");
13+
});
14+
}
15+
16+
exports.smart_escape = smart_escape;

lib/auth_token.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*/
55

66
const crypto = require('crypto');
7+
const smart_escape = require('./utils/smart_escape').smart_escape;
8+
9+
const unsafe = /([ "#%&'/:;<=>?@[\]^`{|}~]+)/g;
710

811
function digest(message, key) {
912
return crypto.createHmac("sha256", Buffer.from(key, "hex")).update(message).digest('hex');
@@ -15,7 +18,8 @@ function digest(message, key) {
1518
* @return {string} escaped url
1619
*/
1720
function escapeToLower(url) {
18-
return encodeURIComponent(url).replace(/%../g, function (match) {
21+
const safeUrl = smart_escape(url, unsafe);
22+
return safeUrl.replace(/%../g, function (match) {
1923
return match.toLowerCase();
2024
});
2125
}
@@ -41,6 +45,7 @@ function escapeToLower(url) {
4145
*/
4246
module.exports = function (options) {
4347
const tokenName = options.token_name ? options.token_name : "__cld_token__";
48+
const tokenSeparator = "~";
4449
if (options.expiration == null) {
4550
if (options.duration != null) {
4651
let start = options.start_time != null ? options.start_time : Math.round(Date.now() / 1000);
@@ -61,11 +66,11 @@ module.exports = function (options) {
6166
tokenParts.push(`acl=${escapeToLower(options.acl)}`);
6267
}
6368
let toSign = [...tokenParts];
64-
if (options.url) {
69+
if (options.url != null && options.acl == null) {
6570
let url = escapeToLower(options.url);
6671
toSign.push(`url=${url}`);
6772
}
68-
let auth = digest(toSign.join("~"), options.key);
73+
let auth = digest(toSign.join(tokenSeparator), options.key);
6974
tokenParts.push(`hmac=${auth}`);
70-
return `${tokenName}=${tokenParts.join('~')}`;
75+
return `${tokenName}=${tokenParts.join(tokenSeparator)}`;
7176
};

lib/utils/index.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const isNumber = require("lodash/isNumber");
2929
const isObject = require("lodash/isObject");
3030
const isString = require("lodash/isString");
3131
const isUndefined = require("lodash/isUndefined");
32+
const smart_escape = require("./smart_escape").smart_escape;
3233

3334
const config = require("../config");
3435
const generate_token = require("../auth_token");
@@ -1008,16 +1009,6 @@ function unsigned_url_prefix(
10081009
}
10091010
return prefix;
10101011
}
1011-
// Based on CGI::unescape. In addition does not escape / :
1012-
// smart_escape = (string)->
1013-
// encodeURIComponent(string).replace(/%3A/g, ":").replace(/%2F/g, "/")
1014-
function smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/g) {
1015-
return string.replace(unsafe, function (match) {
1016-
return match.split("").map(function (c) {
1017-
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
1018-
}).join("");
1019-
});
1020-
}
10211012

10221013
function api_url(action = 'upload', options = {}) {
10231014
let cloudinary = ensureOption(options, "upload_prefix", "https://api.cloudinary.com");

lib/utils/smart_escape.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Based on CGI::unescape. In addition does not escape / :
2+
// smart_escape = (string)->
3+
// encodeURIComponent(string).replace(/%3A/g, ":").replace(/%2F/g, "/")
4+
function smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/g) {
5+
return string.replace(unsafe, function (match) {
6+
return match.split("").map(function (c) {
7+
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
8+
}).join("");
9+
});
10+
}
11+
12+
exports.smart_escape = smart_escape;

test/authtoken_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe("authToken", function () {
8989
width: 300,
9090
},
9191
});
92-
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=7d276841d70c4ecbd0708275cd6a82e1f08e47838fbb0bceb2538e06ddfa3029");
92+
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=55cfe516530461213fe3b3606014533b1eca8ff60aeab79d1bb84c9322eebc1f");
9393
});
9494
it("should compute expiration as start time + duration", function () {
9595
let token = {

0 commit comments

Comments
 (0)