Browse Source

Merge branch 'master' into feat/post-comment-notification

Sou Mizobuchi 7 years ago
parent
commit
6c70a27c55

+ 4 - 0
CHANGES.md

@@ -3,8 +3,12 @@ CHANGES
 
 ## 3.1.12-RC
 
+* Feature: Add XSS Settings
+* Improvement: Prevent XSS in various situations
+* Improvement: Add overlay styles for pasting file to comment form
 * Fix: Omit unnecessary css link
     * Introduced by 3.1.10
+* Fix: Invitation mail do not be sent
 
 ## 3.1.11
 

+ 0 - 1
config/webpack.dll.js

@@ -12,7 +12,6 @@ module.exports = {
       // Libraries
       'axios',
       'babel-polyfill',
-      'bootstrap-select',
       'browser-bunyan', 'bunyan-format',
       'codemirror', 'react-codemirror2',
       'clipboard',

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

@@ -127,7 +127,7 @@ module.exports = function(crowi, app) {
 
   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.csrfKeyGenerator(crowi, app));

+ 5 - 2
lib/crowi/index.js

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

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

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

+ 4 - 1
lib/models/config.js

@@ -461,9 +461,12 @@ module.exports = function(crowi) {
       customTitle = '{{page}} - {{sitename}}';
     }
 
-    return customTitle
+    // replace
+    customTitle = customTitle
       .replace('{{sitename}}', this.appTitle(config))
       .replace('{{page}}', page);
+
+    return crowi.xss.process(customTitle);
   };
 
   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) {
-    var Page = this
+    const Page = this
       , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
-      , grant = options.grant || GRANT_PUBLIC
       , redirectTo = options.redirectTo || null
       , grantUserGroupId = options.grantUserGroupId || null;
 
+    let grant = options.grant || GRANT_PUBLIC;
+
+    // sanitize path
+    path = crowi.xss.process(path);
+
     // force public
     if (isPortalPath(path)) {
       grant = GRANT_PUBLIC;
@@ -1001,7 +1005,7 @@ module.exports = function(crowi) {
           throw new Error('Cannot create new page to existed path');
         }
 
-        var newPage = new Page();
+        const newPage = new Page();
         newPage.path = path;
         newPage.creator = user;
         newPage.lastUpdateUser = user;
@@ -1249,11 +1253,14 @@ module.exports = function(crowi) {
   };
 
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
-    var Page = this
+    const Page = this
       , Revision = crowi.model('Revision')
       , path = pageData.path
       , 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 を変更
       .then((data) => {
@@ -1264,7 +1271,7 @@ module.exports = function(crowi) {
         pageData.path = newPagePath;
 
         if (createRedirectPage) {
-          var body = 'redirect ' + newPagePath;
+          const body = 'redirect ' + newPagePath;
           Page.create(path, body, user, {redirectTo: newPagePath});
         }
         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) {
-    var Page = this
+    const Page = this
       , path = pageData.path
       , pathRegExp = new RegExp('^' + escapeStringRegexp(path), 'i');
 
+    // sanitize path
+    newPagePathPrefix = crowi.xss.process(newPagePathPrefix);
+
     return Page.generateQueryToListWithDescendants(path, user, options)
       .then(function(pages) {
         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) {
     const query = { name: name };

+ 29 - 27
lib/routes/admin.js

@@ -591,15 +591,15 @@ module.exports = function(crowi, app) {
 
   // グループ詳細
   actions.userGroup.detail = function(req, res) {
-    var name = req.params.name;
-    var renderVar = {
+    const userGroupId = req.params.id;
+    const renderVar = {
       userGroup: null,
       userGroupRelations: [],
       pageGroupRelations: [],
       notRelatedusers: []
     };
-    var targetUserGroup = null;
-    UserGroup.findUserGroupByName(name)
+    let targetUserGroup = null;
+    UserGroup.findOne({ _id: userGroupId})
       .then(function(userGroup) {
         targetUserGroup = userGroup;
         if (targetUserGroup == null) {
@@ -636,18 +636,20 @@ module.exports = function(crowi, app) {
 
   //グループの生成
   actions.userGroup.create = function(req, res) {
-    var form = req.form.createGroupForm;
+    const form = req.form.createGroupForm;
     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 {
       req.flash('errorMessage', req.form.errors.join('\n'));
@@ -658,8 +660,8 @@ module.exports = function(crowi, app) {
   //
   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)
     .then((userGroupData) => {
@@ -688,7 +690,7 @@ module.exports = function(crowi, app) {
       }
     })
     .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) {
 
-    var userGroupId = req.params.userGroupId;
+    const userGroupId = req.params.userGroupId;
     let userGroupName = null;
 
     UserGroup.findById(userGroupId)
@@ -775,7 +777,7 @@ module.exports = function(crowi, app) {
     .then((updated) => {
       req.flash('successMessage', 'Deleted group picture');
 
-      return res.redirect('/admin/user-group-detail/' + userGroupName);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     })
     .catch((err) => {
       debug('An error occured.', err);
@@ -785,7 +787,7 @@ module.exports = function(crowi, app) {
         return res.redirect('/admin/user-groups/');
       }
       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);
     })
     .then((result) => {
-      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+      return res.redirect('/admin/user-group-detail/' + userGroup.id);
     }).catch((err) => {
       debug('Error on create user-group relation', err);
       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) {
     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)
     .then(() =>{
-      return res.redirect('/admin/user-group-detail/' + name);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     })
     .catch((err) => {
       debug('Error on remove user-group-relation', err);
@@ -1122,6 +1123,7 @@ module.exports = function(crowi, app) {
     debug('mailer setup for validate SMTP setting', smtpClient);
 
     smtpClient.sendMail({
+      from: form['mail:from'],
       to: req.user.email,
       subject: 'Wiki管理設定のアップデートによるメール通知',
       text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。'

+ 2 - 2
lib/routes/index.js

@@ -122,7 +122,7 @@ module.exports = function(crowi, app) {
 
   // user-groups admin
   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/: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);
@@ -131,7 +131,7 @@ module.exports = function(crowi, app) {
 
   // 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/: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/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
   const generateGravatarSrc = function(user) {
@@ -139,7 +139,7 @@ exports.swigFilters = function(app, swig) {
 
     swig.setFilter('datetz', function(input, format) {
       // timezone
-      var swigFilters = require('swig-templates/lib/filters');
+      const swigFilters = require('swig-templates/lib/filters');
       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);
     });
 
+    swig.setFilter('preventXss', function(string) {
+      return crowi.xss.process(string);
+    });
+
     next();
   };
 };

+ 25 - 25
lib/util/swigFunctions.js

@@ -1,5 +1,5 @@
 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')
     , Page = crowi.model('Page')
     , Config = crowi.model('Config')
@@ -45,15 +45,15 @@ module.exports = function(crowi, app, req, locals) {
    * return app title
    */
   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
    */
   locals.isEnabledPassport = function() {
-    var config = crowi.getConfig();
+    const config = crowi.getConfig();
     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
    */
   locals.isLdapSetup = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     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
    */
   locals.isLdapSetupFailed = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
   };
 
@@ -88,17 +88,17 @@ module.exports = function(crowi, app, req, locals) {
       return false;
     }
 
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
 
   locals.passportGoogleLoginEnabled = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return locals.isEnabledPassport() && config.crowi['security:passport-google:isEnabled'];
   };
 
   locals.passportGitHubLoginEnabled = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
   };
 
@@ -110,17 +110,17 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isEnabledPlugins = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPlugins(config);
   };
 
   locals.isEnabledLinebreaks = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledLinebreaks(config);
   };
 
   locals.isEnabledLinebreaksInComments = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledLinebreaksInComments(config);
   };
 
@@ -133,12 +133,12 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.customHeader = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.customHeader(config);
   };
 
   locals.theme = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.theme(config);
   };
 
@@ -148,37 +148,37 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.behaviorType = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.behaviorType(config);
   };
 
   locals.layoutType = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.layoutType(config);
   };
 
   locals.highlightJsStyle = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.highlightJsStyle(config);
   };
 
   locals.highlightJsStyleBorder = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.highlightJsStyleBorder(config);
   };
 
   locals.isEnabledTimeline = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledTimeline(config);
   };
 
   locals.isUploadable = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isUploadable(config);
   };
 
   locals.isEnabledAttachTitleHeader = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledAttachTitleHeader(config);
   };
 
@@ -203,7 +203,7 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isTopPage = function() {
-    var path = req.path || '';
+    let path = req.path || '';
     if (path === '/') {
       return true;
     }
@@ -212,7 +212,7 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isTrashPage = function() {
-    var path = req.path || '';
+    let path = req.path || '';
     if (path.match(/^\/trash\/.*/)) {
       return true;
     }
@@ -221,8 +221,8 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   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);
   };

+ 10 - 14
lib/util/xss.js

@@ -3,36 +3,32 @@ class Xss {
   constructor(xssOption) {
     const xss = require('xss');
 
-    const isEnabledXssPrevention = xssOption.isEnabledXssPrevention;
-    const tagWhiteList = xssOption.tagWhiteList;
-    const attrWhiteList = xssOption.attrWhiteList;
+    xssOption = xssOption || {};
+
+    const tagWhiteList = xssOption.tagWhiteList || [];
+    const attrWhiteList = xssOption.attrWhiteList || [];
 
     let whiteListContent = {};
 
     // default
     let option = {
       stripIgnoreTag: true,
-      stripIgnoreTagBody: false,
+      stripIgnoreTagBody: false,    // see https://github.com/weseek/growi/pull/505
       css: false,
       whiteList: whiteListContent,
       escapeHtml: (html) => html,   // resolve https://github.com/weseek/growi/issues/221
     };
 
-    if (isEnabledXssPrevention) {
-      tagWhiteList.forEach(tag => {
-        whiteListContent[tag] = attrWhiteList;
-      });
-    }
-    else {
-      option['stripIgnoreTag'] = false;
-    }
+    tagWhiteList.forEach(tag => {
+      whiteListContent[tag] = attrWhiteList;
+    });
 
     // create the XSS Filter instance
     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>
 
   <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()) }}">
   <div class="page-editor-footer form-submit-group form-group form-inline
       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' %}
 
-{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) }}{% endblock %}
+{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">
   <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>
 </div>
 {% endblock %}
@@ -199,7 +199,7 @@
                   <i class="icon-settings"></i> <span class="caret"></span>
                 </button>
                 <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() }}">
                   </form>
                   <li>

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

@@ -119,12 +119,12 @@
         </thead>
         <tbody>
           {% for sGroup in userGroups %}
-          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup.name %}
+          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup._id.toString() %}
           <tr>
             <td>
               <img src="{{ sGroup|picture }}" class="picture img-circle" />
             </td>
-            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name }}</a></td>
+            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name | preventXss }}</a></td>
             <td><ul class="list-inline">
               {% for relation in userGroupRelations.get(sGroup) %}
               <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
@@ -146,7 +146,7 @@
                   <li>
                     <a href="#"
                         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-toggle="modal">
                       <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">
           <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="create-page-input-row d-flex align-items-center">
                 <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 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 %}"
   >
 

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

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

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

@@ -16,7 +16,7 @@
   <div class="tab-content">
 
     {% 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 #}
       <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>
         </div>
-        <script type="text/template">{{ page.revision.body.toString() | sanitize }}</script>
+        <script type="text/template">{{ page.revision.body.toString() | encodeHTML }}</script>
       </div>
       <hr>
       {% endfor %}

+ 8 - 2
resource/js/app.js

@@ -4,6 +4,8 @@ import { I18nextProvider } from 'react-i18next';
 
 import i18nFactory from './i18n';
 
+import Xss from '../../lib/util/xss';
+
 import Crowi from './util/Crowi';
 // import CrowiRenderer from './util/CrowiRenderer';
 import GrowiRenderer from './util/GrowiRenderer';
@@ -25,7 +27,7 @@ import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
-import NewPageNameInputter from './components/NewPageNameInputter';
+import NewPageNameInput from './components/NewPageNameInput';
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -40,6 +42,10 @@ if (!window) {
 const userlang = $('body').data('userlang');
 const i18n = i18nFactory(userlang);
 
+// setup xss library
+const xss = new Xss();
+window.xss = xss;
+
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
 let pageRevisionId = null;
@@ -114,7 +120,7 @@ const componentMappings = {
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   '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

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

@@ -8,6 +8,9 @@ export default class CopyButton extends React.Component {
     super(props);
 
     this.showToolTip = this.showToolTip.bind(this);
+
+    // retrieve xss library from window
+    this.xss = window.xss;
   }
 
   showToolTip() {
@@ -27,12 +30,14 @@ export default class CopyButton extends React.Component {
       verticalAlign: 'text-top',
     }, this.props.buttonStyle);
 
+    const text = this.xss.process(this.props.text);
+
     return (
       <span className="btn-copy-container" style={containerStyle}>
         <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-style={style}
-            data-clipboard-text={this.props.text} onSuccess={this.showToolTip}>
+            data-clipboard-text={text} onSuccess={this.showToolTip}>
 
           <i className={this.props.iconClassName}></i>
         </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';
 
-export default class NewPageNameInputter extends React.Component {
+export default class NewPageNameInput extends React.Component {
 
   constructor(props) {
 
@@ -59,11 +59,11 @@ export default class NewPageNameInputter extends React.Component {
   }
 }
 
-NewPageNameInputter.propTypes = {
+NewPageNameInput.propTypes = {
   crowi:          PropTypes.object.isRequired,
   parentPageName: PropTypes.string,
 };
 
-NewPageNameInputter.defaultProps = {
+NewPageNameInput.defaultProps = {
   parentPageName: '',
 };

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

@@ -13,6 +13,9 @@ export default class RevisionPath extends React.Component {
       isListPage: false,
       isLinkToListPage: true,
     };
+
+    // retrieve xss library from window
+    this.xss = window.xss;
   }
 
   componentWillMount() {
@@ -37,7 +40,7 @@ export default class RevisionPath extends React.Component {
     splitted.forEach((pageName) => {
       pages.push({
         pagePath: parentPath + encodeURIComponent(pageName),
-        pageName: pageName,
+        pageName: this.xss.process(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 {
 
+  constructor(props) {
+    super(props);
+
+    // retrieve xss library from window
+    this.xss = window.xss;
+  }
+
   render() {
     const buttonStyle = {
       fontSize: '1em'
     };
 
+    const pagePath = this.xss.process(this.props.pagePath);
+
     const url = (this.props.pageId == null)
       ? decodeURIComponent(location.href)
       : `${location.origin}/${this.props.pageId}`;
-    const copiedText = this.props.pagePath + '\n' + url;
+    const copiedText = pagePath + '\n' + url;
 
     return (
       <span>

+ 4 - 18
resource/js/components/PageEditor/Editor.js

@@ -185,22 +185,9 @@ export default class Editor extends AbstractEditor {
     return className;
   }
 
-  getOverlayStyle() {
-    return {
-      position: 'absolute',
-      zIndex: 4,  // forward than .CodeMirror-gutters
-      top: 0,
-      right: 0,
-      bottom: 0,
-      left: 0,
-    };
-  }
-
   renderDropzoneOverlay() {
-    const overlayStyle = this.getOverlayStyle();
-
     return (
-      <div style={overlayStyle} className="overlay">
+      <div className="overlay">
         {this.state.isUploading &&
           <span className="overlay-content">
             <div className="speeding-wheel d-inline-block"></div>
@@ -221,8 +208,8 @@ export default class Editor extends AbstractEditor {
 
     const isMobile = this.props.isMobile;
 
-    return <React.Fragment>
-      <div style={flexContainer}>
+    return (
+      <div style={flexContainer} className="editor-container">
         <Dropzone
             ref="dropzone"
             disableClick
@@ -270,8 +257,7 @@ export default class Editor extends AbstractEditor {
         </button>
 
       </div>
-
-    </React.Fragment>;
+    );
   }
 
 }

+ 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.hideSelectGroupModal = this.hideSelectGroupModal.bind(this);
 
@@ -81,7 +84,7 @@ class GrantSelector extends React.Component {
 
   getGroupName() {
     const pageGrantGroup = this.state.pageGrantGroup;
-    return pageGrantGroup ? pageGrantGroup.name : '';
+    return pageGrantGroup ? this.xss.process(pageGrantGroup.name) : '';
   }
 
   /**

+ 125 - 0
resource/styles/scss/_editor-overlay.scss

@@ -0,0 +1,125 @@
+.editor-container {
+  .overlay {
+    // layout
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    position: absolute;
+    z-index: 7;  // forward than .CodeMirror-vscrollbar
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .overlay-content {
+    padding: 0.5em;
+  }
+
+  .page-editor-editor-container {
+    .overlay-content {
+      font-size: 2.5em;
+    }
+  }
+
+  @mixin overlay-processing-style() {
+    .overlay {
+      background: rgba(255,255,255,0.5);
+    }
+    .overlay-content {
+      padding: 0.3em;
+      background: rgba(200,200,200,0.5);
+      color: #444;
+    }
+  }
+  // add icon on cursor
+  .autoformat-markdown-table-activated .CodeMirror-cursor {
+    &:after {
+      font-family: 'FontAwesome';
+      content: '\f0ce';
+    }
+  }
+
+  // for Dropzone
+  .dropzone {
+    @mixin insertSimpleLineIcons($code) {
+      &:before {
+        margin-right: 0.2em;
+        font-family: 'simple-line-icons';
+        content: $code;
+      }
+    }
+
+    position: relative;   // against .overlay position: absolute
+
+    // unuploadable or rejected
+    &.dropzone-unuploadable, &.dropzone-rejected {
+      .overlay {
+        background: rgba(200,200,200,0.8);
+      }
+      .overlay-content {
+        color: #444;
+      }
+    }
+    // uploading
+    &.dropzone-uploading {
+      @include overlay-processing-style();
+    }
+
+    // unuploadable
+    &.dropzone-unuploadable {
+      .overlay-content {
+        // insert content
+        @include insertSimpleLineIcons("\e617");  // icon-exclamation
+        &:after {
+          content: "File uploading is disabled";
+        }
+      }
+    }
+    // uploadable
+    &.dropzone-uploadable {
+      // accepted
+      &.dropzone-accepted:not(.dropzone-rejected) {
+        .overlay {
+          border: 4px dashed #ccc;
+        }
+        .overlay-content {
+          // insert content
+          @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
+          &:after {
+            content: "Drop here to upload";
+          }
+          // style
+          color: #666;
+          background: rgba(200,200,200,0.8);
+        }
+      }
+      // file type mismatch
+      &.dropzone-rejected:not(.dropzone-uploadablefile) .overlay-content {
+        // insert content
+        @include insertSimpleLineIcons("\e032");  // icon-picture
+        &:after {
+          content: "Only an image file is allowed";
+        }
+      }
+      // multiple files
+      &.dropzone-accepted.dropzone-rejected .overlay-content {
+        // insert content
+        @include insertSimpleLineIcons("\e617");  // icon-exclamation
+        &:after {
+          content: "Only 1 file is allowed";
+        }
+      }
+    }
+  } // end of.dropzone
+
+  .textarea-editor {
+    border: none;
+    font-family: monospace;
+  }
+
+  .loading-keymap {
+    @include overlay-processing-style();
+  }
+}

+ 1 - 113
resource/styles/scss/_on-edit.scss

@@ -171,117 +171,6 @@ body.on-edit {
       }
     }
 
-    .overlay {
-      // layout
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      // style
-      margin: 0 15px;
-    }
-    .overlay-content {
-      font-size: 2.5em;
-      padding: 0.5em;
-    }
-
-    @mixin overlay-processing-style() {
-      .overlay {
-        background: rgba(255,255,255,0.5);
-      }
-      .overlay-content {
-        padding: 0.3em;
-        background: rgba(200,200,200,0.5);
-        color: #444;
-      }
-    }
-    // add icon on cursor
-    .autoformat-markdown-table-activated .CodeMirror-cursor {
-      &:after {
-        font-family: 'FontAwesome';
-        content: '\f0ce';
-      }
-    }
-
-    // for Dropzone
-    .dropzone {
-      @mixin insertSimpleLineIcons($code) {
-        &:before {
-          margin-right: 0.2em;
-          font-family: 'simple-line-icons';
-          content: $code;
-        }
-      }
-
-      // unuploadable or rejected
-      &.dropzone-unuploadable, &.dropzone-rejected {
-        .overlay {
-          background: rgba(200,200,200,0.8);
-        }
-        .overlay-content {
-          color: #444;
-        }
-      }
-      // uploading
-      &.dropzone-uploading {
-        @include overlay-processing-style();
-      }
-
-      // unuploadable
-      &.dropzone-unuploadable {
-        .overlay-content {
-          // insert content
-          @include insertSimpleLineIcons("\e617");  // icon-exclamation
-          &:after {
-            content: "File uploading is disabled";
-          }
-        }
-      }
-      // uploadable
-      &.dropzone-uploadable {
-        // accepted
-        &.dropzone-accepted:not(.dropzone-rejected) {
-          .overlay {
-            border: 4px dashed #ccc;
-          }
-          .overlay-content {
-            // insert content
-            @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
-            &:after {
-              content: "Drop here to upload";
-            }
-            // style
-            color: #666;
-            background: rgba(200,200,200,0.8);
-          }
-        }
-        // file type mismatch
-        &.dropzone-rejected:not(.dropzone-uploadablefile) .overlay-content {
-          // insert content
-          @include insertSimpleLineIcons("\e032");  // icon-picture
-          &:after {
-            content: "Only an image file is allowed";
-          }
-        }
-        // multiple files
-        &.dropzone-accepted.dropzone-rejected .overlay-content {
-          // insert content
-          @include insertSimpleLineIcons("\e617");  // icon-exclamation
-          &:after {
-            content: "Only 1 file is allowed";
-          }
-        }
-      }
-    } // end of.dropzone
-
-    .textarea-editor {
-      border: none;
-      font-family: monospace;
-    }
-
-    .loading-keymap {
-      @include overlay-processing-style();
-    }
-
     .btn-open-dropzone {
       z-index: 2;
       font-size: small;
@@ -291,16 +180,15 @@ body.on-edit {
       border: none;
       border-radius: 0;
       border-top: 1px dotted #ccc;
-
       &:active {
         box-shadow: none;
       }
-
       // hide if screen size is less than smartphone
       @media (max-width: $screen-xs) {
         display: none;
       }
     }
+
   }
   .page-editor-preview-container {
   }

+ 1 - 0
resource/styles/scss/style.scss

@@ -21,6 +21,7 @@
 @import 'comment_growi';
 @import 'create-page';
 @import 'create-template';
+@import 'editor-overlay';
 @import 'layout';
 @import 'layout_crowi';
 @import 'layout_crowi_sidebar';