Просмотр исходного кода

Merge pull request #534 from weseek/imprv/prevent-xss-3

Imprv/prevent xss 3
Yuki Takei 7 лет назад
Родитель
Сommit
7c970a8b06

+ 1 - 1
lib/crowi/express-init.js

@@ -127,7 +127,7 @@ module.exports = function(crowi, app) {
 
 
   app.use(flash());
   app.use(flash());
 
 
-  app.use(middleware.swigFilters(app, swig));
+  app.use(middleware.swigFilters(crowi, app, swig));
   app.use(middleware.swigFunctions(crowi, app));
   app.use(middleware.swigFunctions(crowi, app));
 
 
   app.use(middleware.csrfKeyGenerator(crowi, app));
   app.use(middleware.csrfKeyGenerator(crowi, app));

+ 5 - 2
lib/crowi/index.js

@@ -1,7 +1,7 @@
 'use strict';
 'use strict';
 
 
 
 
-var debug = require('debug')('growi:crowi')
+const debug = require('debug')('growi:crowi')
   , logger = require('@alias/logger')('growi:crowi')
   , logger = require('@alias/logger')('growi:crowi')
   , pkg = require('@root/package.json')
   , pkg = require('@root/package.json')
   , path = require('path')
   , path = require('path')
@@ -10,10 +10,12 @@ var debug = require('debug')('growi:crowi')
   , mongoose    = require('mongoose')
   , mongoose    = require('mongoose')
 
 
   , models = require('../models')
   , models = require('../models')
+
+  , Xss = require('../util/xss')
   ;
   ;
 
 
 function Crowi(rootdir, env) {
 function Crowi(rootdir, env) {
-  var self = this;
+  const self = this;
 
 
   this.version = pkg.version;
   this.version = pkg.version;
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
@@ -35,6 +37,7 @@ function Crowi(rootdir, env) {
   this.mailer = {};
   this.mailer = {};
   this.interceptorManager = {};
   this.interceptorManager = {};
   this.passportService = null;
   this.passportService = null;
+  this.xss = new Xss();
 
 
   this.tokens = null;
   this.tokens = null;
 
 

+ 1 - 1
lib/form/admin/userGroupCreate.js

@@ -1,6 +1,6 @@
 'use strict';
 'use strict';
 
 
-var form = require('express-form')
+const form = require('express-form')
   , field = form.field;
   , field = form.field;
 
 
 module.exports = form(
 module.exports = form(

+ 4 - 1
lib/models/config.js

@@ -461,9 +461,12 @@ module.exports = function(crowi) {
       customTitle = '{{page}} - {{sitename}}';
       customTitle = '{{page}} - {{sitename}}';
     }
     }
 
 
-    return customTitle
+    // replace
+    customTitle = customTitle
       .replace('{{sitename}}', this.appTitle(config))
       .replace('{{sitename}}', this.appTitle(config))
       .replace('{{page}}', page);
       .replace('{{page}}', page);
+
+    return crowi.xss.process(customTitle);
   };
   };
 
 
   configSchema.statics.behaviorType = function(config) {
   configSchema.statics.behaviorType = function(config) {

+ 17 - 7
lib/models/page.js

@@ -982,13 +982,17 @@ module.exports = function(crowi) {
   };
   };
 
 
   pageSchema.statics.create = function(path, body, user, options) {
   pageSchema.statics.create = function(path, body, user, options) {
-    var Page = this
+    const Page = this
       , Revision = crowi.model('Revision')
       , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
       , format = options.format || 'markdown'
-      , grant = options.grant || GRANT_PUBLIC
       , redirectTo = options.redirectTo || null
       , redirectTo = options.redirectTo || null
       , grantUserGroupId = options.grantUserGroupId || null;
       , grantUserGroupId = options.grantUserGroupId || null;
 
 
+    let grant = options.grant || GRANT_PUBLIC;
+
+    // sanitize path
+    path = crowi.xss.process(path);
+
     // force public
     // force public
     if (isPortalPath(path)) {
     if (isPortalPath(path)) {
       grant = GRANT_PUBLIC;
       grant = GRANT_PUBLIC;
@@ -1001,7 +1005,7 @@ module.exports = function(crowi) {
           throw new Error('Cannot create new page to existed path');
           throw new Error('Cannot create new page to existed path');
         }
         }
 
 
-        var newPage = new Page();
+        const newPage = new Page();
         newPage.path = path;
         newPage.path = path;
         newPage.creator = user;
         newPage.creator = user;
         newPage.lastUpdateUser = user;
         newPage.lastUpdateUser = user;
@@ -1249,11 +1253,14 @@ module.exports = function(crowi) {
   };
   };
 
 
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
-    var Page = this
+    const Page = this
       , Revision = crowi.model('Revision')
       , Revision = crowi.model('Revision')
       , path = pageData.path
       , path = pageData.path
       , createRedirectPage = options.createRedirectPage || 0
       , createRedirectPage = options.createRedirectPage || 0
-      , moveUnderTrees     = options.moveUnderTrees || 0;
+      ;
+
+    // sanitize path
+    newPagePath = crowi.xss.process(newPagePath);
 
 
     return Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath, lastUpdateUser: user})  // pageData の path を変更
     return Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath, lastUpdateUser: user})  // pageData の path を変更
       .then((data) => {
       .then((data) => {
@@ -1264,7 +1271,7 @@ module.exports = function(crowi) {
         pageData.path = newPagePath;
         pageData.path = newPagePath;
 
 
         if (createRedirectPage) {
         if (createRedirectPage) {
-          var body = 'redirect ' + newPagePath;
+          const body = 'redirect ' + newPagePath;
           Page.create(path, body, user, {redirectTo: newPagePath});
           Page.create(path, body, user, {redirectTo: newPagePath});
         }
         }
         pageEvent.emit('update', pageData, user); // update as renamed page
         pageEvent.emit('update', pageData, user); // update as renamed page
@@ -1274,10 +1281,13 @@ module.exports = function(crowi) {
   };
   };
 
 
   pageSchema.statics.renameRecursively = function(pageData, newPagePathPrefix, user, options) {
   pageSchema.statics.renameRecursively = function(pageData, newPagePathPrefix, user, options) {
-    var Page = this
+    const Page = this
       , path = pageData.path
       , path = pageData.path
       , pathRegExp = new RegExp('^' + escapeStringRegexp(path), 'i');
       , pathRegExp = new RegExp('^' + escapeStringRegexp(path), 'i');
 
 
+    // sanitize path
+    newPagePathPrefix = crowi.xss.process(newPagePathPrefix);
+
     return Page.generateQueryToListWithDescendants(path, user, options)
     return Page.generateQueryToListWithDescendants(path, user, options)
       .then(function(pages) {
       .then(function(pages) {
         return Promise.all(pages.map(function(page) {
         return Promise.all(pages.map(function(page) {

+ 0 - 6
lib/models/user-group.js

@@ -82,12 +82,6 @@ class UserGroup {
       });
       });
   }
   }
 
 
-  // TBD: グループ名によるグループ検索
-  static findUserGroupByName(name) {
-    const query = { name: name };
-    return this.findOne(query);
-  }
-
   // 登録可能グループ名確認
   // 登録可能グループ名確認
   static isRegisterableName(name) {
   static isRegisterableName(name) {
     const query = { name: name };
     const query = { name: name };

+ 28 - 27
lib/routes/admin.js

@@ -591,15 +591,15 @@ module.exports = function(crowi, app) {
 
 
   // グループ詳細
   // グループ詳細
   actions.userGroup.detail = function(req, res) {
   actions.userGroup.detail = function(req, res) {
-    var name = req.params.name;
-    var renderVar = {
+    const userGroupId = req.params.id;
+    const renderVar = {
       userGroup: null,
       userGroup: null,
       userGroupRelations: [],
       userGroupRelations: [],
       pageGroupRelations: [],
       pageGroupRelations: [],
       notRelatedusers: []
       notRelatedusers: []
     };
     };
-    var targetUserGroup = null;
-    UserGroup.findUserGroupByName(name)
+    let targetUserGroup = null;
+    UserGroup.findOne({ _id: userGroupId})
       .then(function(userGroup) {
       .then(function(userGroup) {
         targetUserGroup = userGroup;
         targetUserGroup = userGroup;
         if (targetUserGroup == null) {
         if (targetUserGroup == null) {
@@ -636,18 +636,20 @@ module.exports = function(crowi, app) {
 
 
   //グループの生成
   //グループの生成
   actions.userGroup.create = function(req, res) {
   actions.userGroup.create = function(req, res) {
-    var form = req.form.createGroupForm;
+    const form = req.form.createGroupForm;
     if (req.form.isValid) {
     if (req.form.isValid) {
-      UserGroup.createGroupByName(form.userGroupName)
-      .then((newUserGroup) => {
-        req.flash('successMessage', newUserGroup.name);
-        req.flash('createdUserGroup', newUserGroup);
-        return res.redirect('/admin/user-groups');
-      })
-      .catch((err) => {
-        debug('create userGroup error:', err);
-        req.flash('errorMessage', '同じグループ名が既に存在します。');
-      });
+      const userGroupName = crowi.xss.process(form.userGroupName);
+
+      UserGroup.createGroupByName(userGroupName)
+        .then((newUserGroup) => {
+          req.flash('successMessage', newUserGroup.name);
+          req.flash('createdUserGroup', newUserGroup);
+          return res.redirect('/admin/user-groups');
+        })
+        .catch((err) => {
+          debug('create userGroup error:', err);
+          req.flash('errorMessage', '同じグループ名が既に存在します。');
+        });
     }
     }
     else {
     else {
       req.flash('errorMessage', req.form.errors.join('\n'));
       req.flash('errorMessage', req.form.errors.join('\n'));
@@ -658,8 +660,8 @@ module.exports = function(crowi, app) {
   //
   //
   actions.userGroup.update = function(req, res) {
   actions.userGroup.update = function(req, res) {
 
 
-    var userGroupId = req.params.userGroupId;
-    var name = req.body.name;
+    const userGroupId = req.params.userGroupId;
+    const name = crowi.xss.process(req.body.name);
 
 
     UserGroup.findById(userGroupId)
     UserGroup.findById(userGroupId)
     .then((userGroupData) => {
     .then((userGroupData) => {
@@ -688,7 +690,7 @@ module.exports = function(crowi, app) {
       }
       }
     })
     })
     .then(() => {
     .then(() => {
-      return res.redirect('/admin/user-group-detail/' + name);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     });
     });
   };
   };
 
 
@@ -759,7 +761,7 @@ module.exports = function(crowi, app) {
 
 
   actions.userGroup.deletePicture = function(req, res) {
   actions.userGroup.deletePicture = function(req, res) {
 
 
-    var userGroupId = req.params.userGroupId;
+    const userGroupId = req.params.userGroupId;
     let userGroupName = null;
     let userGroupName = null;
 
 
     UserGroup.findById(userGroupId)
     UserGroup.findById(userGroupId)
@@ -775,7 +777,7 @@ module.exports = function(crowi, app) {
     .then((updated) => {
     .then((updated) => {
       req.flash('successMessage', 'Deleted group picture');
       req.flash('successMessage', 'Deleted group picture');
 
 
-      return res.redirect('/admin/user-group-detail/' + userGroupName);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     })
     })
     .catch((err) => {
     .catch((err) => {
       debug('An error occured.', err);
       debug('An error occured.', err);
@@ -785,7 +787,7 @@ module.exports = function(crowi, app) {
         return res.redirect('/admin/user-groups/');
         return res.redirect('/admin/user-groups/');
       }
       }
       else {
       else {
-        return res.redirect('/admin/user-group-detail/' + userGroupName);
+        return res.redirect('/admin/user-group-detail/' + userGroupId);
       }
       }
     });
     });
   };
   };
@@ -847,23 +849,22 @@ module.exports = function(crowi, app) {
       UserGroupRelation.createRelation(userGroup, user);
       UserGroupRelation.createRelation(userGroup, user);
     })
     })
     .then((result) => {
     .then((result) => {
-      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+      return res.redirect('/admin/user-group-detail/' + userGroup.id);
     }).catch((err) => {
     }).catch((err) => {
       debug('Error on create user-group relation', err);
       debug('Error on create user-group relation', err);
       req.flash('errorMessage', 'Error on create user-group relation');
       req.flash('errorMessage', 'Error on create user-group relation');
-      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+      return res.redirect('/admin/user-group-detail/' + userGroup.id);
     });
     });
   };
   };
 
 
   actions.userGroupRelation.remove = function(req, res) {
   actions.userGroupRelation.remove = function(req, res) {
     const UserGroupRelation = crowi.model('UserGroupRelation');
     const UserGroupRelation = crowi.model('UserGroupRelation');
-    var name = req.params.name;
-    var relationId = req.params.relationId;
+    const userGroupId = req.params.id;
+    const relationId = req.params.relationId;
 
 
-    debug(name, relationId);
     UserGroupRelation.removeById(relationId)
     UserGroupRelation.removeById(relationId)
     .then(() =>{
     .then(() =>{
-      return res.redirect('/admin/user-group-detail/' + name);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     })
     })
     .catch((err) => {
     .catch((err) => {
       debug('Error on remove user-group-relation', err);
       debug('Error on remove user-group-relation', err);

+ 2 - 2
lib/routes/index.js

@@ -122,7 +122,7 @@ module.exports = function(crowi, app) {
 
 
   // user-groups admin
   // user-groups admin
   app.get('/admin/user-groups'             , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
   app.get('/admin/user-groups'             , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
-  app.get('/admin/user-group-detail/:name'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
+  app.get('/admin/user-group-detail/:id'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
   app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
   app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
   app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
   app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
   app.post('/admin/user-group/:userGroupId/picture/delete', loginRequired(crowi, app), admin.userGroup.deletePicture);
   app.post('/admin/user-group/:userGroupId/picture/delete', loginRequired(crowi, app), admin.userGroup.deletePicture);
@@ -131,7 +131,7 @@ module.exports = function(crowi, app) {
 
 
   // user-group-relations admin
   // user-group-relations admin
   app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);
   app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);
-  app.post('/admin/user-group-relation/:name/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove);
+  app.post('/admin/user-group-relation/:id/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove);
 
 
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);

+ 7 - 3
lib/util/middlewares.js

@@ -77,7 +77,7 @@ exports.swigFunctions = function(crowi, app) {
   };
   };
 };
 };
 
 
-exports.swigFilters = function(app, swig) {
+exports.swigFilters = function(crowi, app, swig) {
 
 
   // define a function for Gravatar
   // define a function for Gravatar
   const generateGravatarSrc = function(user) {
   const generateGravatarSrc = function(user) {
@@ -139,7 +139,7 @@ exports.swigFilters = function(app, swig) {
 
 
     swig.setFilter('datetz', function(input, format) {
     swig.setFilter('datetz', function(input, format) {
       // timezone
       // timezone
-      var swigFilters = require('swig-templates/lib/filters');
+      const swigFilters = require('swig-templates/lib/filters');
       return swigFilters.date(input, format, app.get('tzoffset'));
       return swigFilters.date(input, format, app.get('tzoffset'));
     });
     });
 
 
@@ -179,10 +179,14 @@ exports.swigFilters = function(app, swig) {
       }
       }
     });
     });
 
 
-    swig.setFilter('sanitize', function(string) {
+    swig.setFilter('encodeHTML', function(string) {
       return entities.encodeHTML(string);
       return entities.encodeHTML(string);
     });
     });
 
 
+    swig.setFilter('preventXss', function(string) {
+      return crowi.xss.process(string);
+    });
+
     next();
     next();
   };
   };
 };
 };

+ 26 - 26
lib/util/swigFunctions.js

@@ -1,5 +1,5 @@
 module.exports = function(crowi, app, req, locals) {
 module.exports = function(crowi, app, req, locals) {
-  var debug = require('debug')('growi:lib:swigFunctions')
+  const debug = require('debug')('growi:lib:swigFunctions')
     , stringWidth = require('string-width')
     , stringWidth = require('string-width')
     , Page = crowi.model('Page')
     , Page = crowi.model('Page')
     , Config = crowi.model('Config')
     , Config = crowi.model('Config')
@@ -45,15 +45,15 @@ module.exports = function(crowi, app, req, locals) {
    * return app title
    * return app title
    */
    */
   locals.appTitle = function() {
   locals.appTitle = function() {
-    var config = crowi.getConfig();
-    return Config.appTitle(config);
+    const config = crowi.getConfig();
+    return crowi.xss.process(Config.appTitle(config));
   };
   };
 
 
   /**
   /**
    * return true if enabled
    * return true if enabled
    */
    */
   locals.isEnabledPassport = function() {
   locals.isEnabledPassport = function() {
-    var config = crowi.getConfig();
+    const config = crowi.getConfig();
     return Config.isEnabledPassport(config);
     return Config.isEnabledPassport(config);
   };
   };
 
 
@@ -69,7 +69,7 @@ module.exports = function(crowi, app, req, locals) {
    * return true if enabled and strategy has been setup successfully
    * return true if enabled and strategy has been setup successfully
    */
    */
   locals.isLdapSetup = function() {
   locals.isLdapSetup = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && passportService.isLdapStrategySetup;
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && passportService.isLdapStrategySetup;
   };
   };
 
 
@@ -77,7 +77,7 @@ module.exports = function(crowi, app, req, locals) {
    * return true if enabled but strategy has some problem
    * return true if enabled but strategy has some problem
    */
    */
   locals.isLdapSetupFailed = function() {
   locals.isLdapSetupFailed = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
   };
   };
 
 
@@ -88,17 +88,17 @@ module.exports = function(crowi, app, req, locals) {
       return false;
       return false;
     }
     }
 
 
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
   };
 
 
   locals.passportGoogleLoginEnabled = function() {
   locals.passportGoogleLoginEnabled = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return locals.isEnabledPassport() && config.crowi['security:passport-google:isEnabled'];
     return locals.isEnabledPassport() && config.crowi['security:passport-google:isEnabled'];
   };
   };
 
 
   locals.passportGitHubLoginEnabled = function() {
   locals.passportGitHubLoginEnabled = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
     return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
   };
   };
 
 
@@ -110,17 +110,17 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isEnabledPlugins = function() {
   locals.isEnabledPlugins = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPlugins(config);
     return Config.isEnabledPlugins(config);
   };
   };
 
 
   locals.isEnabledLinebreaks = function() {
   locals.isEnabledLinebreaks = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledLinebreaks(config);
     return Config.isEnabledLinebreaks(config);
   };
   };
 
 
   locals.isEnabledLinebreaksInComments = function() {
   locals.isEnabledLinebreaksInComments = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledLinebreaksInComments(config);
     return Config.isEnabledLinebreaksInComments(config);
   };
   };
 
 
@@ -133,12 +133,12 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.customHeader = function() {
   locals.customHeader = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.customHeader(config);
     return Config.customHeader(config);
   };
   };
 
 
   locals.theme = function() {
   locals.theme = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.theme(config);
     return Config.theme(config);
   };
   };
 
 
@@ -148,32 +148,32 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.behaviorType = function() {
   locals.behaviorType = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.behaviorType(config);
     return Config.behaviorType(config);
   };
   };
 
 
   locals.layoutType = function() {
   locals.layoutType = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.layoutType(config);
     return Config.layoutType(config);
   };
   };
 
 
   locals.highlightJsStyle = function() {
   locals.highlightJsStyle = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.highlightJsStyle(config);
     return Config.highlightJsStyle(config);
   };
   };
 
 
   locals.highlightJsStyleBorder = function() {
   locals.highlightJsStyleBorder = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.highlightJsStyleBorder(config);
     return Config.highlightJsStyleBorder(config);
   };
   };
 
 
   locals.isEnabledTimeline = function() {
   locals.isEnabledTimeline = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledTimeline(config);
     return Config.isEnabledTimeline(config);
   };
   };
 
 
   locals.slackConfigured = function() {
   locals.slackConfigured = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
     if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
       return true;
       return true;
     }
     }
@@ -181,12 +181,12 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isUploadable = function() {
   locals.isUploadable = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isUploadable(config);
     return Config.isUploadable(config);
   };
   };
 
 
   locals.isEnabledAttachTitleHeader = function() {
   locals.isEnabledAttachTitleHeader = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledAttachTitleHeader(config);
     return Config.isEnabledAttachTitleHeader(config);
   };
   };
 
 
@@ -211,7 +211,7 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isTopPage = function() {
   locals.isTopPage = function() {
-    var path = req.path || '';
+    let path = req.path || '';
     if (path === '/') {
     if (path === '/') {
       return true;
       return true;
     }
     }
@@ -220,7 +220,7 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isTrashPage = function() {
   locals.isTrashPage = function() {
-    var path = req.path || '';
+    let path = req.path || '';
     if (path.match(/^\/trash\/.*/)) {
     if (path.match(/^\/trash\/.*/)) {
       return true;
       return true;
     }
     }
@@ -229,8 +229,8 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isDeletablePage = function() {
   locals.isDeletablePage = function() {
-    var Page = crowi.model('Page');
-    var path = req.path || '';
+    let Page = crowi.model('Page');
+    let path = req.path || '';
 
 
     return Page.isDeletableName(path);
     return Page.isDeletableName(path);
   };
   };

+ 6 - 4
lib/util/xss.js

@@ -3,8 +3,10 @@ class Xss {
   constructor(xssOption) {
   constructor(xssOption) {
     const xss = require('xss');
     const xss = require('xss');
 
 
-    const tagWhiteList = xssOption.tagWhiteList;
-    const attrWhiteList = xssOption.attrWhiteList;
+    xssOption = xssOption || {};
+
+    const tagWhiteList = xssOption.tagWhiteList || [];
+    const attrWhiteList = xssOption.attrWhiteList || [];
 
 
     let whiteListContent = {};
     let whiteListContent = {};
 
 
@@ -25,8 +27,8 @@ class Xss {
     this.myxss = new xss.FilterXSS(option);
     this.myxss = new xss.FilterXSS(option);
   }
   }
 
 
-  process(markdown) {
-    return this.myxss.process(markdown);
+  process(document) {
+    return this.myxss.process(document);
   }
   }
 
 
 }
 }

+ 1 - 1
lib/views/_form.html

@@ -19,7 +19,7 @@
   <div id="page-editor">{% if pageForm.body %}{{ pageForm.body }}{% endif %}</div>
   <div id="page-editor">{% if pageForm.body %}{{ pageForm.body }}{% endif %}</div>
 
 
   <input type="hidden" id="form-body" name="pageForm[body]" value="{% if pageForm.body %}{{ pageForm.body }}{% endif %}">
   <input type="hidden" id="form-body" name="pageForm[body]" value="{% if pageForm.body %}{{ pageForm.body }}{% endif %}">
-  <input type="hidden" name="pageForm[path]" value="{{ path }}">
+  <input type="hidden" name="pageForm[path]" value="{{ path | preventXss }}">
   <input type="hidden" name="pageForm[currentRevision]" value="{{ pageForm.currentRevision|default(page.revision._id.toString()) }}">
   <input type="hidden" name="pageForm[currentRevision]" value="{{ pageForm.currentRevision|default(page.revision._id.toString()) }}">
   <div class="page-editor-footer form-submit-group form-group form-inline
   <div class="page-editor-footer form-submit-group form-group form-inline
       d-flex align-items-center justify-content-between">
       d-flex align-items-center justify-content-between">

+ 3 - 3
lib/views/admin/user-group-detail.html

@@ -1,11 +1,11 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) }}{% endblock %}
+{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 
 {% block content_header %}
 {% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
-    <h1 class="title" id="">{{ t('UserGroup management') + '/' + userGroup.name }}</h1>
+    <h1 class="title" id="">{{ t('UserGroup management') + '/' + userGroup.name | preventXss }}</h1>
   </header>
   </header>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
@@ -199,7 +199,7 @@
                   <i class="icon-settings"></i> <span class="caret"></span>
                   <i class="icon-settings"></i> <span class="caret"></span>
                 </button>
                 </button>
                 <ul class="dropdown-menu" role="menu">
                 <ul class="dropdown-menu" role="menu">
-                  <form id="form_removeFromGroup_{{ sUser.id }}" action="/admin/user-group-relation/{{userGroup.name}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
+                  <form id="form_removeFromGroup_{{ sUser.id }}" action="/admin/user-group-relation/{{userGroup._id.toString()}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
                   </form>
                   </form>
                   <li>
                   <li>

+ 3 - 3
lib/views/admin/user-groups.html

@@ -119,12 +119,12 @@
         </thead>
         </thead>
         <tbody>
         <tbody>
           {% for sGroup in userGroups %}
           {% for sGroup in userGroups %}
-          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup.name %}
+          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup._id.toString() %}
           <tr>
           <tr>
             <td>
             <td>
               <img src="{{ sGroup|picture }}" class="picture img-circle" />
               <img src="{{ sGroup|picture }}" class="picture img-circle" />
             </td>
             </td>
-            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name }}</a></td>
+            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name | preventXss }}</a></td>
             <td><ul class="list-inline">
             <td><ul class="list-inline">
               {% for relation in userGroupRelations.get(sGroup) %}
               {% for relation in userGroupRelations.get(sGroup) %}
               <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
               <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
@@ -146,7 +146,7 @@
                   <li>
                   <li>
                     <a href="#"
                     <a href="#"
                         data-user-group-id="{{ sGroup._id.toString() }}"
                         data-user-group-id="{{ sGroup._id.toString() }}"
-                        data-user-group-name="{{ sGroup.name.toString() }}"
+                        data-user-group-name="{{ sGroup.name.toString() | encodeHTML }}"
                         data-target="#admin-delete-user-group-modal"
                         data-target="#admin-delete-user-group-modal"
                         data-toggle="modal">
                         data-toggle="modal">
                       <i class="icon-fw icon-fire text-danger"></i> 削除する
                       <i class="icon-fw icon-fire text-danger"></i> 削除する

+ 1 - 1
lib/views/modal/create_page.html

@@ -46,7 +46,7 @@
 
 
         <div id="template-form" class="row form-horizontal m-t-15">
         <div id="template-form" class="row form-horizontal m-t-15">
           <fieldset class="col-xs-12">
           <fieldset class="col-xs-12">
-            <legend>{{ t('template.modal_label.Create template under', parentPath(path)) }}</legend>
+            <legend>{{ t('template.modal_label.Create template under', parentPath(path)) | preventXss }}</legend>
             <div class="d-flex create-page-input-container">
             <div class="d-flex create-page-input-container">
               <div class="create-page-input-row d-flex align-items-center">
               <div class="create-page-input-row d-flex align-items-center">
                 <select id="template-type" class="form-control selectpicker" title="{{ t('template.option_label.select') }}">
                 <select id="template-type" class="form-control selectpicker" title="{{ t('template.option_label.select') }}">

+ 2 - 2
lib/views/widget/not_found_content.html

@@ -8,8 +8,8 @@
 </div>
 </div>
 
 
 <div id="content-main" class="content-main content-main-not-found page-list"
 <div id="content-main" class="content-main content-main-not-found page-list"
-  data-path="{{ path }}"
-  data-path-shortname="{{ path|path2name }}"
+  data-path="{{ path | preventXss }}"
+  data-path-shortname="{{ path|path2name | preventXss }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >
   >
 
 

+ 1 - 1
lib/views/widget/page_alerts.html

@@ -7,7 +7,7 @@
       {% elseif page.grant == 4 %}
       {% elseif page.grant == 4 %}
         <i class="icon-fw icon-lock"></i><strong>{{ consts.pageGrants[page.grant] }}</strong> ({{ t('Browsing of this page is restricted') }})
         <i class="icon-fw icon-lock"></i><strong>{{ consts.pageGrants[page.grant] }}</strong> ({{ t('Browsing of this page is restricted') }})
       {% elseif page.grant == 5 %}
       {% elseif page.grant == 5 %}
-        <i class="icon-fw icon-organization"></i><strong>'{{ pageRelatedGroup.name }}' only</strong> ({{ t('Browsing of this page is restricted') }})
+        <i class="icon-fw icon-organization"></i><strong>'{{ pageRelatedGroup.name | preventXss }}' only</strong> ({{ t('Browsing of this page is restricted') }})
       {% endif %}
       {% endif %}
       </p>
       </p>
     {% endif %}
     {% endif %}

+ 1 - 1
lib/views/widget/page_content.html

@@ -15,7 +15,7 @@
   <div class="tab-content">
   <div class="tab-content">
 
 
     {% if page %}
     {% if page %}
-      <script type="text/template" id="raw-text-original">{{ revision.body.toString() | sanitize }}</script>
+      <script type="text/template" id="raw-text-original">{{ revision.body.toString() | encodeHTML }}</script>
 
 
       {# formatted text #}
       {# formatted text #}
       <div class="tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body">
       <div class="tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body">

+ 1 - 1
lib/views/widget/page_list_and_timeline.html

@@ -33,7 +33,7 @@
             <div class="revision-body wiki"></div>
             <div class="revision-body wiki"></div>
           </div>
           </div>
         </div>
         </div>
-        <script type="text/template">{{ page.revision.body.toString() | sanitize }}</script>
+        <script type="text/template">{{ page.revision.body.toString() | encodeHTML }}</script>
       </div>
       </div>
       <hr>
       <hr>
       {% endfor %}
       {% endfor %}

+ 8 - 2
resource/js/app.js

@@ -4,6 +4,8 @@ import { I18nextProvider } from 'react-i18next';
 
 
 import i18nFactory from './i18n';
 import i18nFactory from './i18n';
 
 
+import Xss from '../../lib/util/xss';
+
 import Crowi from './util/Crowi';
 import Crowi from './util/Crowi';
 // import CrowiRenderer from './util/CrowiRenderer';
 // import CrowiRenderer from './util/CrowiRenderer';
 import GrowiRenderer from './util/GrowiRenderer';
 import GrowiRenderer from './util/GrowiRenderer';
@@ -24,7 +26,7 @@ import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
 import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
 import BookmarkButton   from './components/BookmarkButton';
-import NewPageNameInputter from './components/NewPageNameInputter';
+import NewPageNameInput from './components/NewPageNameInput';
 
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -39,6 +41,10 @@ if (!window) {
 const userlang = $('body').data('userlang');
 const userlang = $('body').data('userlang');
 const i18n = i18nFactory(userlang);
 const i18n = i18nFactory(userlang);
 
 
+// setup xss library
+const xss = new Xss();
+window.xss = xss;
+
 const mainContent = document.querySelector('#content-main');
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
 let pageId = null;
 let pageRevisionId = null;
 let pageRevisionId = null;
@@ -111,7 +117,7 @@ const componentMappings = {
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
 
 
-  'page-name-inputter': <NewPageNameInputter crowi={crowi} parentPageName={pagePath} />,
+  'page-name-inputter': <NewPageNameInput crowi={crowi} parentPageName={pagePath} />,
 
 
 };
 };
 // additional definitions if data exists
 // additional definitions if data exists

+ 6 - 1
resource/js/components/CopyButton.js

@@ -8,6 +8,9 @@ export default class CopyButton extends React.Component {
     super(props);
     super(props);
 
 
     this.showToolTip = this.showToolTip.bind(this);
     this.showToolTip = this.showToolTip.bind(this);
+
+    // retrieve xss library from window
+    this.xss = window.xss;
   }
   }
 
 
   showToolTip() {
   showToolTip() {
@@ -27,12 +30,14 @@ export default class CopyButton extends React.Component {
       verticalAlign: 'text-top',
       verticalAlign: 'text-top',
     }, this.props.buttonStyle);
     }, this.props.buttonStyle);
 
 
+    const text = this.xss.process(this.props.text);
+
     return (
     return (
       <span className="btn-copy-container" style={containerStyle}>
       <span className="btn-copy-container" style={containerStyle}>
         <ClipboardButton className={this.props.buttonClassName}
         <ClipboardButton className={this.props.buttonClassName}
             button-id={this.props.buttonId} button-data-toggle="tooltip" button-data-container="body" button-title="copied!" button-data-placement="bottom" button-data-trigger="manual"
             button-id={this.props.buttonId} button-data-toggle="tooltip" button-data-container="body" button-title="copied!" button-data-placement="bottom" button-data-trigger="manual"
             button-style={style}
             button-style={style}
-            data-clipboard-text={this.props.text} onSuccess={this.showToolTip}>
+            data-clipboard-text={text} onSuccess={this.showToolTip}>
 
 
           <i className={this.props.iconClassName}></i>
           <i className={this.props.iconClassName}></i>
         </ClipboardButton>
         </ClipboardButton>

+ 3 - 3
resource/js/components/NewPageNameInputter.js → resource/js/components/NewPageNameInput.js

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 
 import SearchTypeahead from './SearchTypeahead';
 import SearchTypeahead from './SearchTypeahead';
 
 
-export default class NewPageNameInputter extends React.Component {
+export default class NewPageNameInput extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
 
 
@@ -59,11 +59,11 @@ export default class NewPageNameInputter extends React.Component {
   }
   }
 }
 }
 
 
-NewPageNameInputter.propTypes = {
+NewPageNameInput.propTypes = {
   crowi:          PropTypes.object.isRequired,
   crowi:          PropTypes.object.isRequired,
   parentPageName: PropTypes.string,
   parentPageName: PropTypes.string,
 };
 };
 
 
-NewPageNameInputter.defaultProps = {
+NewPageNameInput.defaultProps = {
   parentPageName: '',
   parentPageName: '',
 };
 };

+ 4 - 1
resource/js/components/Page/RevisionPath.js

@@ -13,6 +13,9 @@ export default class RevisionPath extends React.Component {
       isListPage: false,
       isListPage: false,
       isLinkToListPage: true,
       isLinkToListPage: true,
     };
     };
+
+    // retrieve xss library from window
+    this.xss = window.xss;
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {
@@ -37,7 +40,7 @@ export default class RevisionPath extends React.Component {
     splitted.forEach((pageName) => {
     splitted.forEach((pageName) => {
       pages.push({
       pages.push({
         pagePath: parentPath + encodeURIComponent(pageName),
         pagePath: parentPath + encodeURIComponent(pageName),
-        pageName: pageName,
+        pageName: this.xss.process(pageName),
       });
       });
       parentPath += pageName + '/';
       parentPath += pageName + '/';
     });
     });

+ 10 - 1
resource/js/components/Page/RevisionUrl.js

@@ -5,15 +5,24 @@ import CopyButton from '../CopyButton';
 
 
 export default class RevisionUrl extends React.Component {
 export default class RevisionUrl extends React.Component {
 
 
+  constructor(props) {
+    super(props);
+
+    // retrieve xss library from window
+    this.xss = window.xss;
+  }
+
   render() {
   render() {
     const buttonStyle = {
     const buttonStyle = {
       fontSize: '1em'
       fontSize: '1em'
     };
     };
 
 
+    const pagePath = this.xss.process(this.props.pagePath);
+
     const url = (this.props.pageId == null)
     const url = (this.props.pageId == null)
       ? decodeURIComponent(location.href)
       ? decodeURIComponent(location.href)
       : `${location.origin}/${this.props.pageId}`;
       : `${location.origin}/${this.props.pageId}`;
-    const copiedText = this.props.pagePath + '\n' + url;
+    const copiedText = pagePath + '\n' + url;
 
 
     return (
     return (
       <span>
       <span>

+ 4 - 1
resource/js/components/PageEditor/GrantSelector.js

@@ -43,6 +43,9 @@ class GrantSelector extends React.Component {
       };
       };
     }
     }
 
 
+    // retrieve xss library from window
+    this.xss = window.xss;
+
     this.showSelectGroupModal = this.showSelectGroupModal.bind(this);
     this.showSelectGroupModal = this.showSelectGroupModal.bind(this);
     this.hideSelectGroupModal = this.hideSelectGroupModal.bind(this);
     this.hideSelectGroupModal = this.hideSelectGroupModal.bind(this);
 
 
@@ -81,7 +84,7 @@ class GrantSelector extends React.Component {
 
 
   getGroupName() {
   getGroupName() {
     const pageGrantGroup = this.state.pageGrantGroup;
     const pageGrantGroup = this.state.pageGrantGroup;
-    return pageGrantGroup ? pageGrantGroup.name : '';
+    return pageGrantGroup ? this.xss.process(pageGrantGroup.name) : '';
   }
   }
 
 
   /**
   /**