CVE-2023-23630 (TypeScript, published 2023-02-01T01:15Z)
Description: value: Eta is an embedded JS templating engine that works inside Node, Deno, and the browser. XSS attack - anyone using the Express API is impacted. The problem has been resolved. Users should upgrade to version 2.0.0. As a workaround, don't pass user supplied things directly to `res.render`. 

Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/eta-dev/eta
Files changed:
deno_dist/file-handlers.ts
src/file-handlers.ts
test/file-handlers.spec.ts
Commit message: Don't use data object for Eta configuration (#214)



Vulnerable code:
import { copyProps } from "./utils.ts";
  /** Express.js settings may be stored here */
  settings?: {
    [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
  };
 * - `renderFile(filename, dataAndConfig, [cb])`
 *   - Eta will merge `dataAndConfig` into `eta.config`
 * let rendered = await eta.renderFile("./template", {...data, cache: true})
  However, with Express, configuration options will be passed along with the data.
  Thus, Express will call renderFile with (filename, dataAndOptions, cb)
  And we want to also make (filename, data, options, cb) available
  data = data || {}; // If data is undefined, we don't want accessing data.settings to error
    // Otherwise, get the config from the data object
    // And then grab some config options from data.settings
    // Which is where Express sometimes stores them
    renderConfig = getConfig(data as PartialConfig) as EtaConfigWithFilename;
    if (data.settings) {
      // Pull a few things from known locations
      if (data.settings.views) {
        renderConfig.views = data.settings.views;
      }
      if (data.settings["view cache"]) {
        renderConfig.cache = true;
      }
      // Undocumented after Express 2, but still usable, esp. for
      // items that are unsafe to be passed along with data, like `root`
      const viewOpts = data.settings["view options"];

      if (viewOpts) {
        copyProps(renderConfig, viewOpts);
      }
    }
 * - `renderFile(filename, dataAndConfig, [cb])`
 *   - Eta will merge `dataAndConfig` into `eta.config`
 * let rendered = await eta.renderFile("./template", {...data, cache: true})
import { copyProps } from "./utils.js";
  /** Express.js settings may be stored here */
  settings?: {
    [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
  };
 * - `renderFile(filename, dataAndConfig, [cb])`
 *   - Eta will merge `dataAndConfig` into `eta.config`
 * let rendered = await eta.renderFile("./template", {...data, cache: true})
  However, with Express, configuration options will be passed along with the data.
  Thus, Express will call renderFile with (filename, dataAndOptions, cb)
  And we want to also make (filename, data, options, cb) available
  data = data || {}; // If data is undefined, we don't want accessing data.settings to error
    // Otherwise, get the config from the data object
    // And then grab some config options from data.settings
    // Which is where Express sometimes stores them
    renderConfig = getConfig(data as PartialConfig) as EtaConfigWithFilename;
    if (data.settings) {
      // Pull a few things from known locations
      if (data.settings.views) {
        renderConfig.views = data.settings.views;
      }
      if (data.settings["view cache"]) {
        renderConfig.cache = true;
      }
      // Undocumented after Express 2, but still usable, esp. for
      // items that are unsafe to be passed along with data, like `root`
      const viewOpts = data.settings["view options"];

      if (viewOpts) {
        copyProps(renderConfig, viewOpts);
      }
    }
 * - `renderFile(filename, dataAndConfig, [cb])`
 *   - Eta will merge `dataAndConfig` into `eta.config`
 * let rendered = await eta.renderFile("./template", {...data, cache: true})
    renderFile(fakeFilePath, { cache: true }, function (_err: Error | null, res?: string) {

  it("parses a simple template w/ settings from Express", async () => {
    renderFile(
      filePath,
      {
        name: "<p>Ben</p>",
        cache: true,
        settings: {
          views: [path.join(__dirname, "templates"), path.join(__dirname, "othertemplates")],
          "view cache": true,
          "view options": { autoEscape: false },
        },
      },
      function (_err: Error | null, res?: string) {
        expect(res).toEqual("Hi <p>Ben</p>");
      }
    );
  });
    function cb(err: Error, _res?: string) {
      expect(err.message).toMatch(
    renderFile(errFilePath, { name: "Ada Lovelace", async: true }, cb);

Fixed code:
 * - `renderFile(filename, data, [cb])`
  Express will call renderFile with (filename, data, cb)
  We also want to make (filename, data, options, cb) available
  data = data || {};
    // Otherwise, get the default config
    renderConfig = getConfig({}) as EtaConfigWithFilename;
 * - `renderFile(filename, data, [cb])`
 * - `renderFile(filename, data, [cb])`
  Express will call renderFile with (filename, data, cb)
  We also want to make (filename, data, options, cb) available
  data = data || {};
    // Otherwise, get the default config
    renderConfig = getConfig({}) as EtaConfigWithFilename;
 * - `renderFile(filename, data, [cb])`
    renderFile(fakeFilePath, {}, { cache: true }, function (_err: Error | null, res?: string) {
    function cb(err: Error | null, _res?: string) {
      expect(err?.message).toMatch(
    renderFile(errFilePath, { name: "Ada Lovelace" }, { async: true }, cb);