Skip to content
This repository was archived by the owner on Feb 5, 2024. It is now read-only.

Commit e3d4366

Browse files
committed
CSRF Protection
1 parent 35888b1 commit e3d4366

22 files changed

+142
-59
lines changed

lib/crowi/express-init.js

+2-17
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
'use strict';
22

33
module.exports = function(crowi, app) {
4-
var express = require('express')
4+
var debug = require('debug')('crowi:crowi:express-init')
5+
, express = require('express')
56
, bodyParser = require('body-parser')
67
, multer = require('multer')
7-
, morgan = require('morgan')
88
, cookieParser = require('cookie-parser')
99
, methodOverride = require('method-override')
10-
, errorHandler = require('errorhandler')
1110
, session = require('express-session')
1211
, basicAuth = require('basic-auth-connect')
1312
, flash = require('connect-flash')
@@ -98,18 +97,4 @@ module.exports = function(crowi, app) {
9897

9998
app.use(middleware.loginChecker(crowi, app));
10099

101-
if (env == 'development') {
102-
swig.setDefaults({ cache: false });
103-
app.use(errorHandler({ dumpExceptions: true, showStack: true }));
104-
app.use(morgan('dev'));
105-
}
106-
107-
if (env == 'production') {
108-
var oneYear = 31557600000;
109-
app.use(morgan('combined'));
110-
app.use(function (err, req, res, next) {
111-
res.status(500);
112-
res.render('500', { error: err });
113-
});
114-
}
115100
};

lib/crowi/index.js

+43
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ function Crowi (rootdir, env)
3131
this.searcher = null;
3232
this.mailer = {};
3333

34+
this.tokens = null;
35+
this.csrfToken = null;
36+
this.csrfSecret = null;
3437

3538
this.models = {};
3639

@@ -81,6 +84,8 @@ Crowi.prototype.init = function() {
8184
return self.setupMailer();
8285
}).then(function() {
8386
return self.setupSlack();
87+
}).then(function() {
88+
return self.setupCsrf();
8489
}).then(function() {
8590
return self.buildServer();
8691
});
@@ -242,7 +247,27 @@ Crowi.prototype.setupSlack = function() {
242247
});
243248
};
244249

250+
Crowi.prototype.setupCsrf = function() {
251+
var Tokens = require('csrf');
252+
var tokens = this.tokens = new Tokens();
253+
254+
this.csrfSecret = tokens.secretSync();
255+
this.csrfToken = tokens.create(this.csrfSecret);
256+
257+
return Promise.resolve();
258+
};
259+
260+
Crowi.prototype.getTokens = function() {
261+
return this.tokens;
262+
};
263+
264+
Crowi.prototype.getCsrfSecret = function() {
265+
return this.csrfSecret;
266+
};
245267

268+
Crowi.prototype.getCsrfToken = function() {
269+
return this.csrfToken;
270+
};
246271

247272
Crowi.prototype.start = function() {
248273
var self = this
@@ -267,12 +292,30 @@ Crowi.prototype.start = function() {
267292

268293
Crowi.prototype.buildServer = function() {
269294
var express = require('express')
295+
, errorHandler = require('errorhandler')
296+
, morgan = require('morgan')
270297
, app = express()
298+
, env = this.node_env
271299
;
272300

273301
require('./express-init')(this, app);
274302
require('../routes')(this, app);
275303

304+
if (env == 'development') {
305+
//swig.setDefaults({ cache: false });
306+
app.use(errorHandler({ dumpExceptions: true, showStack: true }));
307+
app.use(morgan('dev'));
308+
}
309+
310+
if (env == 'production') {
311+
var oneYear = 31557600000;
312+
app.use(morgan('combined'));
313+
app.use(function (err, req, res, next) {
314+
res.status(500);
315+
res.render('500', { error: err });
316+
});
317+
}
318+
276319
return new Promise.resolve(app);
277320
};
278321

lib/routes/index.js

+30-29
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,21 @@ module.exports = function(crowi, app) {
1515
, search = require('./search')(crowi, app)
1616
, loginRequired = middleware.loginRequired
1717
, accessTokenParser = middleware.accessTokenParser
18+
, csrf = middleware.csrfVerify(crowi, app)
1819
;
1920

2021
app.get('/' , loginRequired(crowi, app) , page.pageListShow);
2122

2223
app.get('/installer' , middleware.applicationNotInstalled() , installer.index);
23-
app.post('/installer/createAdmin' , middleware.applicationNotInstalled() , form.register , installer.createAdmin);
24+
app.post('/installer/createAdmin' , middleware.applicationNotInstalled() , form.register , csrf, installer.createAdmin);
2425
//app.post('/installer/user' , middleware.applicationNotInstalled() , installer.createFirstUser);
2526

2627
app.get('/login/error/:reason' , login.error);
2728
app.get('/login' , middleware.applicationInstalled() , login.login);
2829
app.get('/login/invited' , login.invited);
29-
app.post('/login/activateInvited' , form.invited , login.invited);
30-
app.post('/login' , form.login , login.login);
31-
app.post('/register' , form.register , login.register);
30+
app.post('/login/activateInvited' , form.invited , csrf, login.invited);
31+
app.post('/login' , form.login , csrf, login.login);
32+
app.post('/register' , form.register , csrf, login.register);
3233
app.get('/register' , middleware.applicationInstalled() , login.register);
3334
app.post('/register/google' , login.registerGoogle);
3435
app.get('/google/callback' , login.googleCallback);
@@ -38,32 +39,32 @@ module.exports = function(crowi, app) {
3839

3940
app.get('/admin' , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
4041
app.get('/admin/app' , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
41-
app.post('/_api/admin/settings/app' , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.app, admin.api.appSetting);
42+
app.post('/_api/admin/settings/app' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.app, admin.api.appSetting);
4243
app.post('/_api/admin/settings/sec' , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.sec, admin.api.appSetting);
43-
app.post('/_api/admin/settings/mail' , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.mail, admin.api.appSetting);
44-
app.post('/_api/admin/settings/aws' , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.aws, admin.api.appSetting);
45-
app.post('/_api/admin/settings/google', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.google, admin.api.appSetting);
46-
app.post('/_api/admin/settings/fb' , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.fb , admin.api.appSetting);
44+
app.post('/_api/admin/settings/mail' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
45+
app.post('/_api/admin/settings/aws' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
46+
app.post('/_api/admin/settings/google', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.google, admin.api.appSetting);
47+
app.post('/_api/admin/settings/fb' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.fb , admin.api.appSetting);
4748

4849
// search admin
4950
app.get('/admin/search' , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);
50-
app.post('/admin/search/build' , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.buildIndex);
51+
app.post('/admin/search/build' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.search.buildIndex);
5152

5253
// notification admin
5354
app.get('/admin/notification' , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.index);
54-
app.post('/admin/notification/slackSetting', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.slackSetting, admin.notification.slackSetting);
55+
app.post('/admin/notification/slackSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.slackSetting, admin.notification.slackSetting);
5556
app.get('/admin/notification/slackAuth' , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.slackAuth);
56-
app.post('/_api/admin/notification.add' , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.notificationAdd);
57-
app.post('/_api/admin/notification.remove' , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.notificationRemove);
57+
app.post('/_api/admin/notification.add' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationAdd);
58+
app.post('/_api/admin/notification.remove' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationRemove);
5859

5960
app.get('/admin/users' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
60-
app.post('/admin/user/invite' , form.admin.userInvite , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.invite);
61-
app.post('/admin/user/:id/makeAdmin' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.makeAdmin);
61+
app.post('/admin/user/invite' , form.admin.userInvite , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.invite);
62+
app.post('/admin/user/:id/makeAdmin' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.makeAdmin);
6263
app.post('/admin/user/:id/removeFromAdmin', loginRequired(crowi, app) , middleware.adminRequired() , admin.user.removeFromAdmin);
63-
app.post('/admin/user/:id/activate' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.activate);
64-
app.post('/admin/user/:id/suspend' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.suspend);
65-
app.post('/admin/user/:id/remove' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.remove);
66-
app.post('/admin/user/:id/removeCompletely' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.removeCompletely);
64+
app.post('/admin/user/:id/activate' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.activate);
65+
app.post('/admin/user/:id/suspend' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.suspend);
66+
app.post('/admin/user/:id/remove' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.remove);
67+
app.post('/admin/user/:id/removeCompletely' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.removeCompletely);
6768

6869
app.get('/me' , loginRequired(crowi, app) , me.index);
6970
app.get('/me/password' , loginRequired(crowi, app) , me.password);
@@ -97,27 +98,27 @@ module.exports = function(crowi, app) {
9798
app.get('/_api/pages.get' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.get);
9899
app.get('/_api/pages.updatePost' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.getUpdatePost);
99100
app.post('/_api/pages.seen' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.seen);
100-
app.post('/_api/pages.rename' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.rename);
101-
app.post('/_api/pages.remove' , loginRequired(crowi, app) , page.api.remove); // (Avoid from API Token)
102-
app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , page.api.revertRemove); // (Avoid from API Token)
101+
app.post('/_api/pages.rename' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, page.api.rename);
102+
app.post('/_api/pages.remove' , loginRequired(crowi, app) , csrf, page.api.remove); // (Avoid from API Token)
103+
app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
103104
app.get('/_api/comments.get' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , comment.api.get);
104-
app.post('/_api/comments.add' , form.comment, accessTokenParser(crowi, app) , loginRequired(crowi, app) , comment.api.add);
105+
app.post('/_api/comments.add' , form.comment, accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, comment.api.add);
105106
app.get( '/_api/bookmarks.get' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , bookmark.api.get);
106-
app.post('/_api/bookmarks.add' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , bookmark.api.add);
107-
app.post('/_api/bookmarks.remove' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , bookmark.api.remove);
108-
app.post('/_api/likes.add' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.like);
109-
app.post('/_api/likes.remove' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.unlike);
107+
app.post('/_api/bookmarks.add' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, bookmark.api.add);
108+
app.post('/_api/bookmarks.remove' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, bookmark.api.remove);
109+
app.post('/_api/likes.add' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, page.api.like);
110+
app.post('/_api/likes.remove' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, page.api.unlike);
110111

111112
app.get( '/_api/revisions.get' , accessTokenParser(crowi, app) , loginRequired(crowi, app) , revision.api.get);
112113
app.get( '/_api/revisions.list' , accessTokenParser(crowi, app) , loginRequired(crowi, app) ,revision.api.list);
113114

114115
//app.get('/_api/revision/:id' , user.useUserData() , revision.api.get);
115116
//app.get('/_api/r/:revisionId' , user.useUserData() , page.api.get);
116117

117-
app.post('/_/edit' , form.revision , loginRequired(crowi, app) , page.pageEdit);
118+
app.post('/_/edit' , form.revision , loginRequired(crowi, app) , csrf, page.pageEdit);
118119
app.get('/trash/$' , loginRequired(crowi, app) , page.deletedPageListShow);
119120
app.get('/trash/*/$' , loginRequired(crowi, app) , page.deletedPageListShow);
120121
app.get('/*/$' , loginRequired(crowi, app) , page.pageListShow);
121122
app.get('/*' , loginRequired(crowi, app) , page.pageShow);
122-
//app.get('/*/edit' , routes.edit);
123+
123124
};

lib/util/middlewares.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ exports.loginChecker = function(crowi, app) {
2323
};
2424
};
2525

26+
exports.csrfVerify = function(crowi, app) {
27+
return function(req, res, next) {
28+
var token = req.body._csrf || req.query._csrf || null;
29+
if (req.skipCsrfVerify) {
30+
return next();
31+
}
32+
33+
if (crowi.getTokens().verify(crowi.getCsrfSecret(), token)) {
34+
return next();
35+
}
36+
37+
return res.sendStatus(403);
38+
};
39+
};
40+
2641
exports.swigFunctions = function(crowi, app) {
2742
return function(req, res, next) {
2843
require('../util/swigFunctions')(crowi, app, req, res.locals);
@@ -138,7 +153,7 @@ exports.loginRequired = function(crowi, app) {
138153

139154
exports.accessTokenParser = function(crowi, app) {
140155
return function(req, res, next) {
141-
var accessToken = req.query.access_token;
156+
var accessToken = req.query.access_token || req.body.access_token || null;
142157
if (!accessToken) {
143158
return next();
144159
}
@@ -148,6 +163,8 @@ exports.accessTokenParser = function(crowi, app) {
148163
User.findUserByApiToken(accessToken)
149164
.then(function(userData) {
150165
req.user = userData;
166+
req.skipCsrfVerify = true;
167+
151168
next();
152169
}).catch(function(err) {
153170
next();

lib/util/swigFunctions.js

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ module.exports = function(crowi, app, req, locals) {
55
, User = crowi.model('User')
66
;
77

8+
// token getter
9+
locals._csrf = function() {
10+
return crowi.getCsrfToken();
11+
};
12+
813
locals.facebookLoginEnabled = function() {
914
var config = crowi.getConfig()
1015
return config.crowi['facebook:appId'] && config.crowi['facebook:secret'];

lib/views/500.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Error: {{ error }}
1+
Error: {{ error.message }}

lib/views/_form.html

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
{% endfor %}
5050
</select>
5151
{% endif %}
52+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
5253
<input type="submit" class="btn btn-primary" id="edit-form-submit" value="ページを更新" />
5354
</div>
5455
</div>

lib/views/admin/app.html

+6-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ <h1 class="title" id="">アプリ設定</h1>
5454

5555
<div class="form-group">
5656
<div class="col-xs-offset-3 col-xs-6">
57+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
5758
<button type="submit" class="btn btn-primary">更新</button>
5859
</div>
5960
</div>
@@ -105,6 +106,7 @@ <h1 class="title" id="">アプリ設定</h1>
105106

106107
<div class="form-group">
107108
<div class="col-xs-offset-3 col-xs-6">
109+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
108110
<button type="submit" class="btn btn-primary">更新</button>
109111
</div>
110112
</div>
@@ -149,6 +151,7 @@ <h1 class="title" id="">アプリ設定</h1>
149151

150152
<div class="form-group">
151153
<div class="col-xs-offset-3 col-xs-6">
154+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
152155
<button type="submit" class="btn btn-primary">更新</button>
153156
</div>
154157
</div>
@@ -197,6 +200,7 @@ <h1 class="title" id="">アプリ設定</h1>
197200

198201
<div class="form-group">
199202
<div class="col-xs-offset-3 col-xs-6">
203+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
200204
<button type="submit" class="btn btn-primary">更新</button>
201205
</div>
202206
</div>
@@ -225,6 +229,7 @@ <h1 class="title" id="">アプリ設定</h1>
225229

226230
<div class="form-group">
227231
<div class="col-xs-offset-3 col-xs-6">
232+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
228233
<button type="submit" class="btn btn-primary">更新</button>
229234
</div>
230235
</div>
@@ -253,6 +258,7 @@ <h1 class="title" id="">アプリ設定</h1>
253258

254259
<div class="form-group">
255260
<div class="col-xs-offset-3 col-xs-6">
261+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
256262
<button type="submit" class="btn btn-primary">更新</button>
257263
</div>
258264
</div>
@@ -297,7 +303,6 @@ <h1 class="title" id="">アプリ設定</h1>
297303
$button.attr('disabled', 'disabled');
298304
var jqxhr = $.post($form.attr('action'), $form.serialize(), function(data)
299305
{
300-
console.log(data);
301306
if (data.status) {
302307
showMessage($id, '更新しました');
303308
} else {

lib/views/admin/notification.html

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ <h1 class="title" id="">通知設定</h1>
6565
</div>
6666
</div>
6767
</fieldset>
68+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
6869
</form>
6970

7071
{% if hasSlackConfig %}
@@ -108,6 +109,7 @@ <h4>Default Notification Settings for Patterns</h4>
108109
</p>
109110
</td>
110111
<td>
112+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
111113
<input type="submit" value="Add" class="btn btn-primary">
112114
</td>
113115
</tr>
@@ -124,6 +126,7 @@ <h4>Default Notification Settings for Patterns</h4>
124126
<td>
125127
<form class="admin-remove-updatepost">
126128
<input type="hidden" name="id" value="{{ notif._id.toString() }}">
129+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
127130
<input type="submit" value="Delete" class="btn btn-default">
128131
</form>
129132
</td>

lib/views/admin/search.html

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ <h1 class="title" id="">検索管理</h1>
5151
</div>
5252
</div>
5353
</fieldset>
54+
<input type="hidden" name="_csrf" value="{{ _csrf() }}">
5455
</form>
5556

5657
</div>

0 commit comments

Comments
 (0)