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

Merge pull request #10173 from weseek/fix/156800-167362-csrf-protection-origin

fix: Csrf protecte origin
Yuki Takei 8 месяцев назад
Родитель
Сommit
b000f5f6e4

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

@@ -3,6 +3,7 @@ import csrf from 'csurf';
 import qs from 'qs';
 
 import { PLUGIN_EXPRESS_STATIC_DIR, PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
+import CertifyOrigin from '~/server/middlewares/certify-origin';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
@@ -26,7 +27,6 @@ module.exports = function(crowi, app) {
   const registerSafeRedirect = registerSafeRedirectFactory();
   const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
   const autoReconnectToS2sMsgServer = require('../middlewares/auto-reconnect-to-s2s-msg-server')(crowi);
-
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
 
   const env = crowi.node_env;
@@ -123,6 +123,8 @@ module.exports = function(crowi, app) {
   // 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(CertifyOrigin);
+
   // passport
   logger.debug('initialize Passport');
   app.use(passport.initialize());

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

@@ -0,0 +1,34 @@
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { NextFunction, Response } from 'express';
+
+import type { AccessTokenParserReq } from '~/server/middlewares/access-token-parser/interfaces';
+import { configManager } from '~/server/service/config-manager';
+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 = (req: AccessTokenParserReq, res: Response & { apiv3Err: Apiv3ErrFunction }, next: NextFunction): void => {
+
+  const appSiteUrl = configManager.getConfig('app:siteUrl');
+
+  const isSameOriginReq = req.headers.origin == null || req.headers.origin === appSiteUrl;
+  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;

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

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