Skip to content

Commit db29448

Browse files
authored
Merge pull request #35 from Rob--W/security-and-reliability
Some security and reliability improvements
2 parents 441d67c + c77b5b2 commit db29448

File tree

4 files changed

+133
-17
lines changed

4 files changed

+133
-17
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,18 @@ REST API
1414
| Close Webpage | DELETE | http://localhost:\<port\> | |
1515

1616
By default, `<port>` is 8090
17+
18+
Environment variables
19+
---------------------
20+
21+
* `INSTANT_MARKDOWN_OPEN_TO_THE_WORLD=1` - by default, the server only listens
22+
on localhost. To make the server available to others in your network, set this
23+
environment variable to a non-empty value. Only use this setting on trusted
24+
networks!
25+
26+
* `INSTANT_MARKDOWN_ALLOW_UNSAFE_CONTENT=1` - by default, scripts are blocked.
27+
Use this preference to allow scripts.
28+
29+
* `INSTANT_MARKDOWN_BLOCK_EXTERNAL=1` - by default, external resources such as
30+
images, stylesheets, frames and plugins are *allowed*. Use this setting to
31+
*block* such external content.

index.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@
1313
margin: 0 auto;
1414
padding: 30px;
1515
}
16+
#con-error {
17+
position: fixed;
18+
top: 0px;
19+
right: 0px;
20+
padding: 5px;
21+
background: white;
22+
color: red;
23+
}
1624
</style>
1725
<script src="/socket.io/socket.io.js"></script>
1826
<script>
1927
var socket = io.connect('http://'+ window.location.host +'/');
2028
socket.on('connect', function () {
29+
setDisconnected(false);
2130
socket.on('newContent', function(newHTML) {
2231
document.querySelector(".markdown-body").innerHTML = newHTML;
2332
});
@@ -32,6 +41,25 @@
3241
document.body.innerHTML = firefoxWarning;
3342
});
3443
});
44+
socket.on('disconnect', function() {
45+
setDisconnected(true);
46+
});
47+
48+
try {
49+
eval('// If CSP is active, then this is blocked');
50+
} catch (e) {
51+
// Detected that the CSP was active (by the user's preference).
52+
// Drop capabilities to prevent rendered markdown from executing scripts.
53+
var meta = document.createElement('meta');
54+
meta.setAttribute('http-equiv', 'Content-Security-Policy');
55+
meta.setAttribute('content', "script-src 'none';");
56+
document.head.appendChild(meta);
57+
}
58+
59+
function setDisconnected(isDisconnected) {
60+
document.getElementById('con-error').style.display =
61+
isDisconnected ? 'block' : 'none';
62+
}
3563
</script>
3664
</head>
3765
<body>
@@ -46,5 +74,6 @@
4674
</div>
4775
</div>
4876
</div>
77+
<div id="con-error" style="display:none">Live preview is unavailable</div>
4978
</body>
5079
</html>

instant-markdown-d

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ var hljs = require('highlight.js');
66
var server = require('http').createServer(httpHandler),
77
exec = require('child_process').exec,
88
io = require('socket.io').listen(server),
9-
send = require('send'),
10-
server,
11-
socket;
9+
send = require('send');
1210

13-
server.listen(8090);
11+
// WARNING: By setting this environment variable, anyone on your network may
12+
// run arbitrary code in your browser and read arbitrary files in the working
13+
// directory of the open file!
14+
if (process.env.INSTANT_MARKDOWN_OPEN_TO_THE_WORLD) {
15+
// Listen on any interface.
16+
server.listen(8090, onListening).once('error', onServerError);
17+
} else {
18+
// Listen locally.
19+
server.listen(8090, '127.0.0.1', onListening).once('error', onServerError);
20+
}
1421

1522
var md = new MarkdownIt({
1623
html: true,
@@ -28,7 +35,13 @@ var md = new MarkdownIt({
2835
}
2936
});
3037

31-
function writeMarkdown(input, output) {
38+
var lastWrittenMarkdown = '';
39+
function writeMarkdown(body) {
40+
lastWrittenMarkdown = md.render(body);
41+
io.sockets.emit('newContent', lastWrittenMarkdown);
42+
}
43+
44+
function readAllInput(input, callback) {
3245
var body = '';
3346
input.on('data', function(data) {
3447
body += data;
@@ -37,24 +50,59 @@ function writeMarkdown(input, output) {
3750
}
3851
});
3952
input.on('end', function() {
40-
output.emit('newContent', md.render(body));
53+
callback(body);
4154
});
4255
}
4356

57+
function addSecurityHeaders(req, res, isIndexFile) {
58+
var csp = [];
59+
60+
// Cannot use 'self' because Chrome does not treat 'self' as http://host
61+
// when the sandbox directive is set.
62+
var HTTP_HOST = req.headers.host || 'localhost:8090';
63+
var CSP_SELF = 'http://' + HTTP_HOST;
64+
65+
if (!process.env.INSTANT_MARKDOWN_ALLOW_UNSAFE_CONTENT) {
66+
if (isIndexFile) {
67+
// index.html will drop the scripting capabilities upon load.
68+
csp.push('script-src ' + CSP_SELF + " 'unsafe-inline'");
69+
csp.push('sandbox allow-scripts allow-modals allow-forms');
70+
} else {
71+
csp.push('script-src ');
72+
}
73+
}
74+
if (process.env.INSTANT_MARKDOWN_BLOCK_EXTERNAL) {
75+
csp.push('default-src data: ' + CSP_SELF);
76+
csp.push("style-src data: 'unsafe-inline' " + CSP_SELF);
77+
csp.push('connect-src ' + CSP_SELF + ' ws://' + HTTP_HOST);
78+
}
79+
res.setHeader('X-Content-Type-Options', 'nosniff');
80+
res.setHeader('Content-Security-Policy', csp.join('; '));
81+
if (isIndexFile) {
82+
// Never cache the index file, to make sure that changes to the CSP are
83+
// picked up across soft reloads.
84+
res.setHeader('Cache-Control', 'no-store');
85+
}
86+
}
87+
4488
function httpHandler(req, res) {
4589
switch(req.method)
4690
{
4791
case 'GET':
4892
// Example: /my-repo/raw/master/sub-dir/some.png
4993
var githubUrl = req.url.match(/\/[^\/]+\/raw\/[^\/]+\/(.+)/);
5094
if (githubUrl) {
95+
addSecurityHeaders(req, res, false);
5196
// Serve the file out of the current working directory
5297
send(req, githubUrl[1])
5398
.root(process.cwd())
5499
.pipe(res);
55100
return;
56101
}
57102

103+
var isIndexFile = /^\/(index\.html)?(\?|$)/.test(req.url);
104+
addSecurityHeaders(req, res, isIndexFile);
105+
58106
// Otherwise serve the file from the directory this module is in
59107
send(req, req.url)
60108
.root(__dirname)
@@ -70,12 +118,12 @@ function httpHandler(req, res) {
70118
// break;
71119

72120
case 'DELETE':
73-
socket.emit('die');
121+
io.sockets.emit('die');
74122
process.exit();
75123
break;
76124

77125
case 'PUT':
78-
writeMarkdown(req, socket);
126+
readAllInput(req, writeMarkdown);
79127
res.writeHead(200);
80128
res.end();
81129
break;
@@ -84,18 +132,42 @@ function httpHandler(req, res) {
84132
}
85133
}
86134

87-
io.set('log level', 1);
88135
io.sockets.on('connection', function(sock){
89-
socket = sock;
90136
process.stdout.write('connection established!');
91-
writeMarkdown(process.stdin, socket);
92-
process.stdin.resume();
137+
if (lastWrittenMarkdown) {
138+
sock.emit('newContent', lastWrittenMarkdown);
139+
}
93140
});
94141

95142

96-
if (process.platform.toLowerCase().indexOf('darwin') >= 0){
97-
exec('open -g http://localhost:8090', function(error, stdout, stderr){});
143+
function onListening() {
144+
if (process.platform.toLowerCase().indexOf('darwin') >= 0){
145+
exec('open -g http://localhost:8090');
146+
}
147+
else { // assume unix/linux
148+
exec('xdg-open http://localhost:8090');
149+
}
150+
readAllInput(process.stdin, function(body) {
151+
writeMarkdown(body);
152+
});
153+
process.stdin.resume();
98154
}
99-
else { // assume unix/linux
100-
exec('xdg-open http://localhost:8090', function(error, stdout, stderr){});
155+
156+
function onServerError(e) {
157+
if (e.code === 'EADDRINUSE') {
158+
readAllInput(process.stdin, function(body) {
159+
// Forward to existing instant-markdown-d server.
160+
require('http').request({
161+
hostname: 'localhost',
162+
port: 8090,
163+
path: '/',
164+
method: 'PUT',
165+
}).end(body);
166+
});
167+
process.stdin.resume();
168+
return;
169+
}
170+
171+
// Another unexpected error. Raise it again.
172+
throw e;
101173
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
"highlight.js": "^8.4.0",
1414
"markdown-it": "^3.0.3",
1515
"send": "~0.1.0",
16-
"socket.io": ""
16+
"socket.io": "^1.4.5"
1717
}
1818
}

0 commit comments

Comments
 (0)