Skip to content

Commit 6a7d7e2

Browse files
committed
Escape LevalDB namespaces
This commit requires changes in master: - bfe255f - f330ca5 - 573597d
1 parent ab95622 commit 6a7d7e2

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"postcss-loader": "^2.0.10",
7878
"react-test-renderer": "^16.2.0",
7979
"rimraf": "^2.6.2",
80+
"sanitize-filename": "^1.6.1",
8081
"sinon": "^4.2.0",
8182
"source-map-explorer": "^1.5.0",
8283
"webpack": "^3.10.0",

server-websocket.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import crypto from 'crypto';
1616
import { join, sep } from 'path';
1717
import { lstatSync, readdirSync } from 'fs';
1818

19+
import LevelDBLib from './src/server/LevelDBLib';
20+
1921
Y.extend(yWebsocketsServer, yleveldb);
2022

2123
const isDirectory = (source) => {
@@ -46,7 +48,7 @@ const server = http.createServer((req, res) => {
4648
const io = socketIo.listen(server);
4749

4850
const yInstances = {};
49-
const dirs = getDirectories('y-leveldb-databases').map(p => p.split(sep)[1]);
51+
const dirs = getDirectories('y-leveldb-databases').map(p => LevelDBLib.unescapeNamespace(p.split(sep)[1]));
5052
const metadata = dirs.reduce((accumulator, d) => Object.assign(accumulator, { [d]: {} }), {});
5153

5254
function getInstanceOfY(room) {
@@ -55,7 +57,7 @@ function getInstanceOfY(room) {
5557
db: {
5658
name: options.db,
5759
dir: 'y-leveldb-databases',
58-
namespace: room,
60+
namespace: LevelDBLib.escapeNamespace(room),
5961
},
6062
connector: {
6163
name: 'websockets-server',

src/server/LevelDBLib.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const unsafeChars = /[^-_a-zA-Z0-9]/g;
2+
const windowsReserved = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
3+
const escapeSeq = /%([_0-9a-fA-F]+)%/g;
4+
5+
export default class LevelDBLib {
6+
/**
7+
* It escapes a namespace to a safe file name.
8+
* This function is injective.
9+
* @param {string} namespace namespace of Level DB
10+
* @returns {string}
11+
*/
12+
static escapeNamespace(namespace) {
13+
if (!namespace) return '%_%';
14+
if (namespace.match(windowsReserved)) {
15+
return `%_%${namespace}`;
16+
}
17+
return namespace.replace(unsafeChars, substr => `%${substr.charCodeAt(0).toString(16)}%`);
18+
}
19+
/**
20+
* Reversed function of escapeNamespace.
21+
* @param {string} escapedNamedpace file name generated by escapeNamespace
22+
* @returns {string}
23+
*/
24+
static unescapeNamespace(escapedNamedpace) {
25+
return escapedNamedpace.replace(escapeSeq, (substr, group) => {
26+
if (group === '_') return '';
27+
return String.fromCharCode(parseInt(group, 16));
28+
});
29+
}
30+
}

test/server/LevelDBLib.ava.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import test from 'ava';
2+
import sanitize from 'sanitize-filename';
3+
4+
import LevelDBLib from '../../src/server/LevelDBLib';
5+
6+
const unsafePaths = [
7+
// path traversal
8+
'/',
9+
'/etc/passwd',
10+
'.',
11+
'./..',
12+
'./dir/../..',
13+
'..',
14+
'../',
15+
// invalid filename
16+
'',
17+
' ',
18+
'last space ',
19+
'last-dot.',
20+
'con',
21+
'con.txt',
22+
'nul',
23+
// illegal chars
24+
'question?mark?char',
25+
'angle<>char',
26+
'asterisk*char',
27+
'null\0char',
28+
'newline\nchar',
29+
'tab\tchar',
30+
// path separator
31+
'dir/file',
32+
'dir\\file',
33+
];
34+
35+
/** @test {LevelDBLib.escapeNamespace} */
36+
test('escapeNamespace should escape unsafe path', t => {
37+
unsafePaths.forEach(p => {
38+
const escaped = LevelDBLib.escapeNamespace(p);
39+
t.not(p, escaped, 'should escape unsafe filename');
40+
const sanitizedEscaped = sanitize(escaped);
41+
t.is(escaped, sanitizedEscaped, 'escaped namespace must be a safe filename');
42+
});
43+
});
44+
45+
/** @test {LevelDBLib.unescapeNamespace} */
46+
test('unescapeNamespace should unescape path', t => {
47+
unsafePaths.forEach(p => {
48+
const escaped = LevelDBLib.escapeNamespace(p);
49+
const unescaped = LevelDBLib.unescapeNamespace(escaped);
50+
t.is(p, unescaped);
51+
});
52+
});
53+
54+
/** @test {LevelDBLib.escapeNamespace} */
55+
test('escapeNamespace should be injective', t => {
56+
const inputs = new Set([
57+
'',
58+
'%',
59+
'%%',
60+
'%%%',
61+
'%_%',
62+
'%%_%',
63+
'%%__%',
64+
'%1%',
65+
'%01%',
66+
]);
67+
const outputs = new Set();
68+
inputs.forEach(p => {
69+
const escaped = LevelDBLib.escapeNamespace(p);
70+
t.false(outputs.has(escaped), `not injective: ${p} -> ${escaped}`);
71+
outputs.add(escaped);
72+
});
73+
});
74+
75+
/** @test {LevelDBLib.escapeNamespace} */
76+
test('escapeNamespace should handle non-ascii chars', t => {
77+
const inputs = new Set([
78+
'ひらがな',
79+
'カタカナ',
80+
'漢字',
81+
'汉语',
82+
'한글',
83+
'عربی زبان',
84+
]);
85+
inputs.forEach(p => {
86+
const escaped = LevelDBLib.escapeNamespace(p);
87+
t.not(p, escaped, 'should escape');
88+
const sanitizedEscaped = sanitize(escaped);
89+
t.is(escaped, sanitizedEscaped, 'escaped namespace must be a safe filename');
90+
const unescaped = LevelDBLib.unescapeNamespace(escaped);
91+
t.is(p, unescaped, 'should restore to non-ascii');
92+
});
93+
});

yarn.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8789,6 +8789,12 @@ [email protected]:
87898789
version "1.3.0"
87908790
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
87918791

8792+
sanitize-filename@^1.6.1:
8793+
version "1.6.1"
8794+
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a"
8795+
dependencies:
8796+
truncate-utf8-bytes "^1.0.0"
8797+
87928798
sax@^1.1.4, sax@~1.2.1:
87938799
version "1.2.4"
87948800
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -9717,6 +9723,12 @@ trough@^1.0.0:
97179723
version "1.0.1"
97189724
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.1.tgz#a9fd8b0394b0ae8fff82e0633a0a36ccad5b5f86"
97199725

9726+
truncate-utf8-bytes@^1.0.0:
9727+
version "1.0.2"
9728+
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
9729+
dependencies:
9730+
utf8-byte-length "^1.0.1"
9731+
97209732
97219733
version "0.0.0"
97229734
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -10063,6 +10075,10 @@ [email protected]:
1006310075
bindings "~1.2.1"
1006410076
nan "~2.4.0"
1006510077

10078+
utf8-byte-length@^1.0.1:
10079+
version "1.0.4"
10080+
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
10081+
1006610082
1006710083
version "2.1.0"
1006810084
resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.0.tgz#0cfec5c8052d44a23e3aaa908104e8075f95dfd5"

0 commit comments

Comments
 (0)