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

add certify origin and split module

yusa-a 9 месяцев назад
Родитель
Сommit
e436483040

+ 4 - 1
apps/app/src/server/crowi/express-init.js

@@ -3,6 +3,7 @@ import csrf from 'csurf';
 import qs from 'qs';
 import qs from 'qs';
 
 
 import { PLUGIN_EXPRESS_STATIC_DIR, PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { PLUGIN_EXPRESS_STATIC_DIR, PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
+import registerCertifyOrigin from '~/server/middlewares/certify-origin';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 
@@ -26,7 +27,7 @@ module.exports = function(crowi, app) {
   const registerSafeRedirect = registerSafeRedirectFactory();
   const registerSafeRedirect = registerSafeRedirectFactory();
   const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
   const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
   const autoReconnectToS2sMsgServer = require('../middlewares/auto-reconnect-to-s2s-msg-server')(crowi);
   const autoReconnectToS2sMsgServer = require('../middlewares/auto-reconnect-to-s2s-msg-server')(crowi);
-
+  const certifyOrigin = registerCertifyOrigin(crowi);
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
 
 
   const env = crowi.node_env;
   const env = crowi.node_env;
@@ -123,6 +124,8 @@ module.exports = function(crowi, app) {
   // default methods + PUT. See: https://expressjs.com/en/resources/middleware/csurf.html#ignoremethods
   // default methods + PUT. See: https://expressjs.com/en/resources/middleware/csurf.html#ignoremethods
   app.use(csrf({ ignoreMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'DELETE'], cookie: false }));
   app.use(csrf({ ignoreMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'DELETE'], cookie: false }));
 
 
+  app.use(certifyOrigin);
+
   // passport
   // passport
   logger.debug('initialize Passport');
   logger.debug('initialize Passport');
   app.use(passport.initialize());
   app.use(passport.initialize());

+ 1 - 0
apps/app/src/server/middlewares/access-token-parser/interfaces.ts

@@ -11,4 +11,5 @@ type ReqBody = {
 
 
 export interface AccessTokenParserReq extends Request<undefined, undefined, ReqBody, ReqQuery> {
 export interface AccessTokenParserReq extends Request<undefined, undefined, ReqBody, ReqQuery> {
   user?: IUserSerializedSecurely<IUserHasId>,
   user?: IUserSerializedSecurely<IUserHasId>,
+  isSameOriginReq: boolean,
 }
 }

+ 37 - 0
apps/app/src/server/middlewares/certify-origin.ts

@@ -0,0 +1,37 @@
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { NextFunction, Response } from 'express';
+
+import type Crowi from '~/server/crowi';
+import type { AccessTokenParserReq } from '~/server/middlewares/access-token-parser/interfaces';
+import isSimpleRequest from '~/server/util/is-simple-request';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:middleware:certify-origin');
+
+type Apiv3ErrFunction = (error: ErrorV3) => void;
+
+const certifyOrigin = (crowi: Crowi): ((req: AccessTokenParserReq, res: Response & { apiv3Err: Apiv3ErrFunction }, next: NextFunction) => void) => {
+
+  const appSiteUrl = crowi.configManager?.getConfig('crowi', 'app:siteUrl');
+  return (req: AccessTokenParserReq, res: Response & { apiv3Err }, next: NextFunction): void => {
+
+    const isSameOriginReq = req.headers.origin == null || req.headers.origin === appSiteUrl;
+    req.isSameOriginReq = isSameOriginReq;
+    const accessToken = req.query.access_token ?? req.body.access_token;
+
+    if (!isSameOriginReq && req.headers.origin != null && isSimpleRequest(req)) {
+      const message = 'Invalid request (origin check failed but simple request)';
+      logger.error(message);
+      return res.apiv3Err(new ErrorV3(message));
+    }
+
+    if (!isSameOriginReq && accessToken == null && !isSimpleRequest(req)) {
+      const message = 'Invalid request (origin check failed and no access token)';
+      logger.error(message);
+      return res.apiv3Err(new ErrorV3(message));
+    }
+
+    next();
+  };
+};
+export default certifyOrigin;

+ 50 - 0
apps/app/src/server/util/is-simple-request.ts

@@ -0,0 +1,50 @@
+import type { Request } from 'express';
+
+import type { AccessTokenParserReq } from '~/server/middlewares/access-token-parser/interfaces';
+
+const isSimpleRequest = (req: Request | AccessTokenParserReq): boolean => {
+  // 1. Check if the request method is allowed
+  const allowedMethods = ['GET', 'HEAD', 'POST'];
+  if (!allowedMethods.includes(req.method)) {
+    return false;
+  }
+
+  // 2. Check if the request headers are safe
+  const safeRequestHeaders = [
+    'accept',
+    'accept-language',
+    'content-language',
+    'content-type',
+    'range',
+    'referer',
+    'dpr',
+    'downlink',
+    'save-Data',
+    'viewport-Width',
+    'width',
+  ];
+  const nonSafeHeaders = Object.keys(req.headers).filter((header) => {
+    const headerLower = header.toLowerCase();
+    return !safeRequestHeaders.includes(headerLower);
+  });
+
+  if (nonSafeHeaders.length > 0) {
+    return false;
+  }
+
+  // 3. Content-Type is
+  const allowedContentTypes = [
+    'application/x-www-form-urlencoded',
+    'multipart/form-data',
+    'text/plain',
+  ];
+  const contentType = req.headers['content-type'];
+
+  if (contentType != null && !allowedContentTypes.includes(contentType.toLowerCase())) {
+    return false;
+  }
+  // Return true if all conditions are met
+  return true;
+};
+
+export default isSimpleRequest;