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

170342/178601 correct forgot-password, users.js, user-activation.ts, login.js

mariko-h 2 недель назад
Родитель
Сommit
7be66f2367

+ 36 - 4
apps/app/src/server/routes/apiv3/forgot-password.js

@@ -1,3 +1,4 @@
+import nodePath from 'node:path';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { format, subSeconds } from 'date-fns';
@@ -13,6 +14,7 @@ import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import httpErrorHandler from '../../middlewares/http-error-handler';
+import { assertFileNameSafeForBaseDir } from '../../util/safe-path-utils';
 import { checkForgotPasswordEnabledMiddlewareFactory } from '../forgot-password';
 
 const logger = loggerFactory('growi:routes:apiv3:forgotPassword');
@@ -92,13 +94,43 @@ module.exports = (crowi) => {
     url,
     expiredAt,
   ) {
+    const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+    let safeLocale = locale;
+
+    if (!SUPPORTED_LOCALES.includes(safeLocale)) {
+      logger.warn(
+        `Invalid or untrusted locale detected: '${safeLocale}'. Falling back to 'en_US' for safety.`,
+      );
+      safeLocale = 'en_US';
+    }
+
+    try {
+      assertFileNameSafeForBaseDir(safeLocale, crowi.localeDir);
+    } catch (err) {
+      logger.error(
+        `Path traversal attempt detected in locale: '${safeLocale}'. Fallback to 'en_US'.`,
+      );
+      safeLocale = 'en_US';
+    }
+
+    const templatePath = join(
+      crowi.localeDir,
+      `${safeLocale}/notifications/${templateFileName}.ejs`,
+    );
+    const normalizedTemplatePath = nodePath.resolve(templatePath);
+    const baseDir = nodePath.resolve(crowi.localeDir);
+
+    if (!normalizedTemplatePath.startsWith(baseDir)) {
+      logger.error(
+        `Security Alert: Path traversal attempted! Resolved path: ${normalizedTemplatePath}`,
+      );
+      throw new Error('Path traversal detected! Blocking template access.');
+    }
+
     return mailService.send({
       to: email,
       subject: '[GROWI] Password Reset',
-      template: join(
-        crowi.localeDir,
-        `${locale}/notifications/${templateFileName}.ejs`,
-      ),
+      template: templatePath,
       vars: {
         appTitle: appService.getAppTitle(),
         email,

+ 69 - 7
apps/app/src/server/routes/apiv3/user-activation.ts

@@ -14,6 +14,8 @@ import { growiInfoService } from '~/server/service/growi-info';
 import { getTranslation } from '~/server/service/i18next';
 import loggerFactory from '~/utils/logger';
 
+import { assertFileNameSafeForBaseDir } from '../../util/safe-path-utils';
+
 const logger = loggerFactory('growi:routes:apiv3:user-activation');
 
 const PASSOWRD_MINIMUM_NUMBER = 8;
@@ -231,11 +233,42 @@ export const completeRegistrationAction = (crowi: Crowi) => {
             if (isMailerSetup) {
               const admins = await User.findAdmins();
               const appTitle = appService.getAppTitle();
-              const locale = configManager.getConfig('app:globalLang');
-              const template = path.join(
+              const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+              let locale = configManager.getConfig('app:globalLang');
+
+              if (!SUPPORTED_LOCALES.includes(locale)) {
+                logger.warn(
+                  `Invalid or untrusted locale detected: '${locale}'. Falling back to 'en_US' for safety.`,
+                );
+                locale = 'en_US';
+              }
+
+              try {
+                assertFileNameSafeForBaseDir(locale, crowi.localeDir);
+              } catch (_err) {
+                logger.error(
+                  `Path traversal attempt detected in locale: '${locale}'. Fallback to 'en_US'.`,
+                );
+                locale = 'en_US';
+              }
+
+              const templatePath = path.join(
                 crowi.localeDir,
                 `${locale}/admin/userWaitingActivation.ejs`,
               );
+              const normalizedTemplatePath = path.resolve(templatePath);
+              const baseDir = path.resolve(crowi.localeDir);
+
+              if (!normalizedTemplatePath.startsWith(baseDir)) {
+                logger.error(
+                  `Security Alert: Path traversal attempted! Resolved path: ${normalizedTemplatePath}`,
+                );
+                throw new Error(
+                  'Path traversal detected! Blocking template access.',
+                );
+              }
+
+              const template = templatePath;
               const url = growiInfoService.getSiteUrl();
 
               sendEmailToAllAdmins(
@@ -312,7 +345,39 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) {
     throw Error('mailService is not setup');
   }
 
-  const locale = configManager.getConfig('app:globalLang');
+  const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+  let locale = configManager.getConfig('app:globalLang');
+
+  if (!SUPPORTED_LOCALES.includes(locale)) {
+    logger.warn(
+      `Invalid or untrusted locale detected: '${locale}'. Falling back to 'en_US' for safety.`,
+    );
+    locale = 'en_US';
+  }
+
+  try {
+    assertFileNameSafeForBaseDir(locale, localeDir);
+  } catch (_err) {
+    logger.error(
+      `Path traversal attempt detected in locale: '${locale}'. Fallback to 'en_US'.`,
+    );
+    locale = 'en_US';
+  }
+
+  const templatePath = path.join(
+    localeDir,
+    `${locale}/notifications/userActivation.ejs`,
+  );
+  const normalizedTemplatePath = path.resolve(templatePath);
+  const baseDir = path.resolve(localeDir);
+
+  if (!normalizedTemplatePath.startsWith(baseDir)) {
+    logger.error(
+      `Security Alert: Path traversal attempted! Resolved path: ${normalizedTemplatePath}`,
+    );
+    throw new Error('Path traversal detected! Blocking template access.');
+  }
+
   const appUrl = growiInfoService.getSiteUrl();
 
   const userRegistrationOrder =
@@ -329,10 +394,7 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) {
   return mailService.send({
     to: email,
     subject: '[GROWI] User Activation',
-    template: path.join(
-      localeDir,
-      `${locale}/notifications/userActivation.ejs`,
-    ),
+    template: templatePath,
     vars: {
       appTitle: appService.getAppTitle(),
       email,

+ 69 - 10
apps/app/src/server/routes/apiv3/users.js

@@ -1,3 +1,4 @@
+import nodePath from 'node:path';
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
@@ -26,6 +27,7 @@ import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { assertFileNameSafeForBaseDir } from '../../util/safe-path-utils';
 
 const logger = loggerFactory('growi:routes:apiv3:users');
 
@@ -206,7 +208,39 @@ module.exports = (crowi) => {
   const sendEmailByUserList = async (userList) => {
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
-    const locale = configManager.getConfig('app:globalLang');
+    const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+    let locale = configManager.getConfig('app:globalLang');
+
+    if (!SUPPORTED_LOCALES.includes(locale)) {
+      logger.warn(
+        `Invalid or untrusted locale detected: '${locale}'. Falling back to 'en_US' for safety.`,
+      );
+      locale = 'en_US';
+    }
+
+    try {
+      assertFileNameSafeForBaseDir(locale, crowi.localeDir);
+    } catch (_err) {
+      logger.error(
+        `Path traversal attempt detected in locale: '${locale}'. Fallback to 'en_US'.`,
+      );
+      locale = 'en_US';
+    }
+
+    const templatePath = path.join(
+      crowi.localeDir,
+      `${locale}/admin/userInvitation.ejs`,
+    );
+    const normalizedTemplatePath = nodePath.resolve(templatePath);
+    const baseDir = nodePath.resolve(crowi.localeDir);
+
+    if (!normalizedTemplatePath.startsWith(baseDir)) {
+      logger.error(
+        `Security Alert: Path traversal attempted! Resolved path: ${normalizedTemplatePath}`,
+      );
+      throw new Error('Path traversal detected! Blocking template access.');
+    }
+
     const failedToSendEmailList = [];
 
     for (const user of userList) {
@@ -215,10 +249,7 @@ module.exports = (crowi) => {
         await mailService.send({
           to: user.email,
           subject: `Invitation to ${appTitle}`,
-          template: path.join(
-            crowi.localeDir,
-            `${locale}/admin/userInvitation.ejs`,
-          ),
+          template: templatePath,
           vars: {
             email: user.email,
             password: user.password,
@@ -242,15 +273,43 @@ module.exports = (crowi) => {
   const sendEmailByUser = async (user) => {
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
-    const locale = configManager.getConfig('app:globalLang');
+    const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+    let locale = configManager.getConfig('app:globalLang');
+
+    if (!SUPPORTED_LOCALES.includes(locale)) {
+      logger.warn(
+        `Invalid or untrusted locale detected: '${locale}'. Falling back to 'en_US' for safety.`,
+      );
+      locale = 'en_US';
+    }
+
+    try {
+      assertFileNameSafeForBaseDir(locale, crowi.localeDir);
+    } catch (_err) {
+      logger.error(
+        `Path traversal attempt detected in locale: '${locale}'. Fallback to 'en_US'.`,
+      );
+      locale = 'en_US';
+    }
+
+    const templatePath = path.join(
+      crowi.localeDir,
+      `${locale}/admin/userResetPassword.ejs`,
+    );
+    const normalizedTemplatePath = nodePath.resolve(templatePath);
+    const baseDir = nodePath.resolve(crowi.localeDir);
+
+    if (!normalizedTemplatePath.startsWith(baseDir)) {
+      logger.error(
+        `Security Alert: Path traversal attempted! Resolved path: ${normalizedTemplatePath}`,
+      );
+      throw new Error('Path traversal detected! Blocking template access.');
+    }
 
     await mailService.send({
       to: user.email,
       subject: `New password for ${appTitle}`,
-      template: path.join(
-        crowi.localeDir,
-        `${locale}/admin/userResetPassword.ejs`,
-      ),
+      template: templatePath,
       vars: {
         email: user.email,
         password: user.password,

+ 34 - 5
apps/app/src/server/routes/login.js

@@ -4,6 +4,7 @@ import loggerFactory from '~/utils/logger';
 
 import { UserStatus } from '../models/user/conts';
 import { growiInfoService } from '../service/growi-info';
+import { assertFileNameSafeForBaseDir } from '../util/safe-path-utils';
 
 // disable all of linting
 // because this file is a deprecated legacy of Crowi
@@ -22,16 +23,44 @@ module.exports = (crowi, app) => {
     // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
     const admins = await User.findAdmins();
     const appTitle = appService.getAppTitle();
-    const locale = configManager.getConfig('app:globalLang');
+    const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+    let locale = configManager.getConfig('app:globalLang');
+
+    if (!SUPPORTED_LOCALES.includes(locale)) {
+      logger.warn(
+        `Invalid or untrusted locale detected: '${locale}'. Falling back to 'en_US' for safety.`,
+      );
+      locale = 'en_US';
+    }
+
+    try {
+      assertFileNameSafeForBaseDir(locale, crowi.localeDir);
+    } catch (_err) {
+      logger.error(
+        `Path traversal attempt detected in locale: '${locale}'. Fallback to 'en_US'.`,
+      );
+      locale = 'en_US';
+    }
+
+    const templatePath = path.join(
+      crowi.localeDir,
+      `${locale}/admin/userWaitingActivation.ejs`,
+    );
+    const normalizedTemplatePath = path.resolve(templatePath);
+    const baseDir = path.resolve(crowi.localeDir);
+
+    if (!normalizedTemplatePath.startsWith(baseDir)) {
+      logger.error(
+        `Security Alert: Path traversal attempted! Resolved path: ${normalizedTemplatePath}`,
+      );
+      throw new Error('Path traversal detected! Blocking template access.');
+    }
 
     const promises = admins.map((admin) => {
       return mailService.send({
         to: admin.email,
         subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
-        template: path.join(
-          crowi.localeDir,
-          `${locale}/admin/userWaitingActivation.ejs`,
-        ),
+        template: templatePath,
         vars: {
           adminUser: admin,
           createdUser: userData,