Преглед изворни кода

170342-179078 DBを直接汚染された時の対策

mariko-h пре 1 месец
родитељ
комит
fa56c1deaa

+ 8 - 0
apps/app/src/server/routes/apiv3/app-settings/index.ts

@@ -585,6 +585,14 @@ module.exports = (crowi: Crowi) => {
     async (req, res) => {
       const { globalLang } = req.body;
       if (globalLang != null) {
+        const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+
+        if (!SUPPORTED_LOCALES.includes(globalLang)) {
+          const msg = `Invalid global language settings: '${globalLang}' is not supported.`;
+          logger.error(msg, { globalLang });
+          return res.apiv3Err(new ErrorV3(msg, 'invalid-globalLang'));
+        }
+
         if (!isFileNameSafeForBaseDir(globalLang, crowi.localeDir)) {
           const msg =
             'Invalid global language settings: path traversal detected.';

+ 20 - 0
apps/app/src/server/service/global-notification/global-notification-mail.ts

@@ -91,6 +91,16 @@ class GlobalNotificationMailService {
     { comment, oldPath }: GlobalNotificationEventVars,
   ): MailOption {
     let locale = configManager.getConfig('app:globalLang');
+
+    const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
+
+    if (!SUPPORTED_LOCALES.includes(locale)) {
+      _logger.warn(
+        `Invalid or untrusted locale detected in DB: '${locale}'. Falling back to 'en_US' for safety.`,
+      );
+      locale = 'en_US';
+    }
+
     // validate for all events
     if (event == null || page == null || triggeredBy == null) {
       throw new Error(
@@ -112,6 +122,16 @@ class GlobalNotificationMailService {
       `${locale}/notifications/${event}.ejs`,
     );
 
+    const normalizedTemplatePath = nodePath.resolve(template);
+    const baseDir = nodePath.resolve(this.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 path = page.path;
     const appTitle = this.crowi.appService.getAppTitle();
     const siteUrl = growiInfoService.getSiteUrl();

+ 34 - 13
apps/app/src/server/service/installer.ts

@@ -20,6 +20,12 @@ export type AutoInstallOptions = {
   serverDate?: Date;
 };
 
+const SUPPORTED_LOCALES: string[] = ['en_US', 'ja_JP', 'zh_CN'];
+
+const getSafeLang = (lang: string): Lang => {
+  return (SUPPORTED_LOCALES.includes(lang) ? lang : 'en_US') as Lang;
+};
+
 export class InstallerService {
   crowi: Crowi;
 
@@ -45,7 +51,12 @@ export class InstallerService {
     const { pageService } = this.crowi;
 
     try {
-      const markdown = fs.readFileSync(filePath);
+      const normalizedPath = path.resolve(filePath);
+      const baseDir = path.resolve(this.crowi.localeDir);
+      if (!normalizedPath.startsWith(baseDir)) {
+        throw new Error(`Path traversal detected: ${normalizedPath}`);
+      }
+      const markdown = fs.readFileSync(normalizedPath);
       return pageService.forceCreateBySystem(pagePath, markdown.toString(), {});
     } catch (err) {
       logger.error(`Failed to create ${pagePath}`, err);
@@ -57,28 +68,34 @@ export class InstallerService {
     initialPagesCreatedAt?: Date,
   ): Promise<any> {
     const { localeDir } = this.crowi;
+
+    const safeLang = getSafeLang(lang);
+
     // create /Sandbox/*
     /*
      * Keep in this order to
      *   1. avoid creating the same pages
      *   2. avoid difference for order in VRT
      */
-    assertFileNameSafeForBaseDir(lang, localeDir);
-    await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox');
+    assertFileNameSafeForBaseDir(safeLang, localeDir);
+    await this.createPage(
+      path.join(localeDir, safeLang, 'sandbox.md'),
+      '/Sandbox',
+    );
     await this.createPage(
-      path.join(localeDir, lang, 'sandbox-markdown.md'),
+      path.join(localeDir, safeLang, 'sandbox-markdown.md'),
       '/Sandbox/Markdown',
     );
     await this.createPage(
-      path.join(localeDir, lang, 'sandbox-bootstrap5.md'),
+      path.join(localeDir, safeLang, 'sandbox-bootstrap5.md'),
       '/Sandbox/Bootstrap5',
     );
     await this.createPage(
-      path.join(localeDir, lang, 'sandbox-diagrams.md'),
+      path.join(localeDir, safeLang, 'sandbox-diagrams.md'),
       '/Sandbox/Diagrams',
     );
     await this.createPage(
-      path.join(localeDir, lang, 'sandbox-math.md'),
+      path.join(localeDir, safeLang, 'sandbox-math.md'),
       '/Sandbox/Math',
     );
 
@@ -125,11 +142,13 @@ export class InstallerService {
     globalLang: Lang,
     options?: AutoInstallOptions,
   ): Promise<void> {
+    const safeLang = getSafeLang(globalLang);
+
     await configManager.updateConfigs(
       {
         'app:installed': true,
         'app:isV5Compatible': true,
-        'app:globalLang': globalLang,
+        'app:globalLang': safeLang,
       },
       { skipPubsub: true },
     );
@@ -151,14 +170,16 @@ export class InstallerService {
     globalLang: Lang,
     options?: AutoInstallOptions,
   ): Promise<IUser> {
-    await this.initDB(globalLang, options);
-    assertFileNameSafeForBaseDir(globalLang, this.crowi.localeDir);
+    const safeLang = getSafeLang(globalLang);
+
+    await this.initDB(safeLang, options);
+    assertFileNameSafeForBaseDir(safeLang, this.crowi.localeDir);
     const User = mongoose.model<IUser, { createUser }>('User');
 
     // create portal page for '/' before creating admin user
     try {
       await this.createPage(
-        path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
+        path.join(this.crowi.localeDir, safeLang, 'welcome.md'),
         '/',
       );
     } catch (err) {
@@ -174,12 +195,12 @@ export class InstallerService {
         username,
         email,
         password,
-        globalLang,
+        safeLang,
       );
       await (adminUser as any).asyncGrantAdmin();
 
       // create initial pages
-      await this.createInitialPages(globalLang, options?.serverDate);
+      await this.createInitialPages(safeLang, options?.serverDate);
 
       return adminUser;
     } catch (err) {