This affects the package phantom-html-to-pdf before 0.6.1.

External Control of File Name or Path

The product allows user input to control or influence paths or file names that are used in filesystem operations.

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

page.onResourceRequested = function (request, networkRequest) {
    console.log('Request ' + request.url);
    if (request.url.lastIndexOf(body.url, 0) === 0) {
        return;
    }

    //potentially dangerous request
    if (request.url.lastIndexOf("file:///", 0) === 0 && !body.allowLocalFilesAccess) {
        networkRequest.abort();
        return;
    }

    //to support cdn like format //cdn.jquery...
    if (request.url.lastIndexOf("file://", 0) === 0 && request.url.lastIndexOf("file:///", 0) !== 0) {
        networkRequest.changeUrl(request.url.replace("file://", "http://"));
    }

    if (body.waitForJS && request.url.lastIndexOf("http://intruct-javascript-ending", 0) === 0) {
        pageJSisDone = true;
    }
};

page.onResourceRequested = function (request, networkRequest) {
    console.log('Request ' + request.url);
    if (request.url.lastIndexOf(body.url, 0) === 0) {
        return;
    }

    //to support cdn like format //cdn.jquery...
    if (request.url.lastIndexOf("file://", 0) === 0 && request.url.lastIndexOf("file:///", 0) !== 0) {
        networkRequest.changeUrl(request.url.replace("file://", "http://"));
    }

     //potentially dangerous request
     if (request.url.lastIndexOf("http://", 0) !== 0 && request.url.lastIndexOf("https://", 0) && !body.allowLocalFilesAccess) {
        networkRequest.abort();
        return;
    }

    if (body.waitForJS && request.url.lastIndexOf("http://intruct-javascript-ending", 0) === 0) {
        pageJSisDone = true;
    }
};

--- a/lib/scripts/conversionScriptPart.js
+++ b/lib/scripts/conversionScriptPart.js
@@ -2,19 +2,19 @@
     console.log('Request ' + request.url);
     if (request.url.lastIndexOf(body.url, 0) === 0) {
         return;
-    }
-
-    //potentially dangerous request
-    if (request.url.lastIndexOf("file:///", 0) === 0 && !body.allowLocalFilesAccess) {
-        networkRequest.abort();
-        return;
-    }
+    }

     //to support cdn like format //cdn.jquery...
     if (request.url.lastIndexOf("file://", 0) === 0 && request.url.lastIndexOf("file:///", 0) !== 0) {
         networkRequest.changeUrl(request.url.replace("file://", "http://"));
     }

+     //potentially dangerous request
+     if (request.url.lastIndexOf("http://", 0) !== 0 && request.url.lastIndexOf("https://", 0) && !body.allowLocalFilesAccess) {
+        networkRequest.abort();
+        return;
+    }
+
     if (body.waitForJS && request.url.lastIndexOf("http://intruct-javascript-ending", 0) === 0) {
         pageJSisDone = true;
     }


clamscan through 1.2.0 is vulnerable to Command Injection. It is possible to inject arbitrary commands as part of the `_is_clamav_binary` function located within `Index.js`. It should be noted that this vulnerability requires a pre-requisite that a folder should be created with the same command that will be chained to execute. This lowers the risk of this issue.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

    async _is_clamav_binary(scanner) {
        const path = this.settings[scanner].path || null;
        if (!path) {
            if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not determine path for clamav binary.`);
            return false;
        }

        const version_cmds = {
            clamdscan: `${path} --version`,
            clamscan: `${path} --version`,
        };

        try {
            await fs_access(path, fs.constants.R_OK);

            const {stdout} = await cp_exec(version_cmds[scanner]);
            if (stdout.toString().match(/ClamAV/) === null) {
                if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`);
                return false;
            }
            return true;
        } catch (err) {
            if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`);
            return false;
        }
    }

    async _is_clamav_binary(scanner) {
        const path = this.settings[scanner].path || null;
        if (!path) {
            if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not determine path for clamav binary.`);
            return false;
        }

        const version_cmds = {
            clamdscan: `${path} --version`,
            clamscan: `${path} --version`,
        };

        try {
            await fs_access(path, fs.constants.R_OK);
            version_cmds_exec = version_cmds[scanner].split(' ');
            const {stdout} = await cp_execfile(version_cmds_exec[0], [version_cmds_exec[1]]);
            if (stdout.toString().match(/ClamAV/) === null) {
                if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`);
                return false;
            }
            return true;
        } catch (err) {
            if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`);
            return false;
        }
    }

--- a/index.js
+++ b/index.js
@@ -12,8 +12,8 @@

         try {
             await fs_access(path, fs.constants.R_OK);
-
-            const {stdout} = await cp_exec(version_cmds[scanner]);
+            version_cmds_exec = version_cmds[scanner].split(' ');
+            const {stdout} = await cp_execfile(version_cmds_exec[0], [version_cmds_exec[1]]);
             if (stdout.toString().match(/ClamAV/) === null) {
                 if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`);
                 return false;

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2020-7613:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/clamscan
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
jest ./poc

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2020-7613:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/clamscan
git apply --whitespace=nowarn /workspace/fix.patch  /workspace/test.patch
npx mocha tests/**/*.js   --grep "should exist|should be a function|should return an object|should not be initialized immediately|should have certain config properties defined|should have the proper global default values set|should have the proper clamscan default values set|should have the proper clamdscan default values set|should accept an options array and merge them with the object defa|should return er|should return true for valid clamscan binary|should return false for invalid clamscan binary"


OpenFGA is an authorization/permission engine built for developers and inspired by Google Zanzibar. During an internal security assessment, it was discovered that OpenFGA version 0.3.0 is vulnerable to authorization bypass under certain conditions. This issue has been patched in version 0.3.1 and is backward compatible.

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.

func (rc *resolutionContext) readUserTuple(ctx context.Context, backend storage.TupleBackend) (*openfgapb.TupleKey, error) {
	tk, ok := rc.contextualTuples.ReadUserTuple(rc.tk)
	if ok {
		return tk, nil
	}

	tuple, err := backend.ReadUserTuple(ctx, rc.store, rc.tk)
	if err != nil {
		return nil, err
	}
	return tuple.GetKey(), nil
}

func (rc *resolutionContext) readUserTuple(ctx context.Context, backend storage.TupleBackend) (*openfgapb.TupleKey, error) {

	typesys := typesystem.New(rc.model)

	tk, ok := rc.contextualTuples.ReadUserTuple(rc.tk)

	if tk != nil {
		err := validation.ValidateTuple(typesys, tk)
		if err == nil && ok {
			return tk, nil
		}
	}

	tuple, err := backend.ReadUserTuple(ctx, rc.store, rc.tk)
	if err != nil {
		return nil, err
	}

	tk = tuple.GetKey()
	if err := validation.ValidateTuple(typesys, tk); err != nil {
		return nil, nil
	}

	return tk, nil
}

--- a/server/commands/check_utils.go
+++ b/server/commands/check_utils.go
@@ -1,12 +1,25 @@
 func (rc *resolutionContext) readUserTuple(ctx context.Context, backend storage.TupleBackend) (*openfgapb.TupleKey, error) {
+
+	typesys := typesystem.New(rc.model)
+
 	tk, ok := rc.contextualTuples.ReadUserTuple(rc.tk)
-	if ok {
-		return tk, nil
+
+	if tk != nil {
+		err := validation.ValidateTuple(typesys, tk)
+		if err == nil && ok {
+			return tk, nil
+		}
 	}

 	tuple, err := backend.ReadUserTuple(ctx, rc.store, rc.tk)
 	if err != nil {
 		return nil, err
 	}
-	return tuple.GetKey(), nil
+
+	tk = tuple.GetKey()
+	if err := validation.ValidateTuple(typesys, tk); err != nil {
+		return nil, nil
+	}
+
+	return tk, nil
 }

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2022-23542:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/openfga
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
cd ./tests/check && go test  -timeout 30s -run ^TestCheckMemory$ github.com/openfga/openfga/tests/check


index.js in the Anton Myshenin aws-lambda-multipart-parser NPM package before 0.1.2 has a Regular Expression Denial of Service (ReDoS) issue via a crafted multipart/form-data boundary string.

Improper Input Validation

The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.

function getValueIgnoringKeyCase(obj, lookedKey) {
    return Object.keys(obj)
        .map(presentKey => presentKey.toLowerCase() === lookedKey.toLowerCase() ? obj[presentKey] : null)
        .filter(item => item)[0];
}

module.exports.parse = (event, spotText) => {
    const boundary = getValueIgnoringKeyCase(event.headers, 'Content-Type').split('=')[1];
    const body = (event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('binary') : event.body)
        .split(new RegExp(boundary))
        .filter(item => item.match(/Content-Disposition/))
        .map((item) => {
            if (item.match(/filename/)) {
                const result = {};
                result[
                    item
                        .match(/name="[a-zA-Z_]+([a-zA-Z0-9_]*)"/)[0]
                        .split('=')[1]
                        .match(/[a-zA-Z_]+([a-zA-Z0-9_]*)/)[0]
                ] = {
                        type: 'file',
                        filename: item
                            .match(/filename="[\w-\. ]+"/)[0]
                            .split('=')[1]
                            .match(/[\w-\.]+/)[0],
                        contentType: item
                            .match(/Content-Type: .+\r\n\r\n/)[0]
                            .replace(/Content-Type: /, '')
                            .replace(/\r\n\r\n/, ''),
                        content: (spotText && item
                            .match(/Content-Type: .+\r\n\r\n/)[0]
                            .replace(/Content-Type: /, '')
                            .replace(/\r\n\r\n/, '')
                            .match(/text/)
                        ) ? item
                            .split(/\r\n\r\n/)[1]
                            .replace(/\r\n\r\n\r\n----/, '') : Buffer.from(item
                                .split(/\r\n\r\n/)[1]
                                .replace(/\r\n\r\n\r\n----/, ''), 'binary'),
                    };
                return result;
            }
            const result = {};
            result[
                item
                    .match(/name="[a-zA-Z_]+([a-zA-Z0-9_]*)"/)[0]
                    .split('=')[1]
                    .match(/[a-zA-Z_]+([a-zA-Z0-9_]*)/)[0]
            ] = item
                .split(/\r\n\r\n/)[1]
                .split(/\r\n--/)[0];
            return result;
        })
        .reduce((accumulator, current) => Object.assign(accumulator, current), {});
    return body;

function getValueIgnoringKeyCase(obj, lookedKey) {
    return Object.keys(obj)
        .map(presentKey => presentKey.toLowerCase() === lookedKey.toLowerCase() ? obj[presentKey] : null)
        .filter(item => item)[0];
}

module.exports.parse = (event, spotText) => {
    const boundary = getValueIgnoringKeyCase(event.headers, 'Content-Type').split('=')[1];
    const body = (event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('binary') : event.body)
        .split(boundary)
        .filter(item => item.match(/Content-Disposition/))
        .map((item) => {
            if (item.match(/filename/)) {
                const result = {};
                result[
                    item
                        .match(/name="[a-zA-Z_]+([a-zA-Z0-9_]*)"/)[0]
                        .split('=')[1]
                        .match(/[a-zA-Z_]+([a-zA-Z0-9_]*)/)[0]
                ] = {
                        type: 'file',
                        filename: item
                            .match(/filename="[\w-\. ]+"/)[0]
                            .split('=')[1]
                            .match(/[\w-\.]+/)[0],
                        contentType: item
                            .match(/Content-Type: .+\r\n\r\n/)[0]
                            .replace(/Content-Type: /, '')
                            .replace(/\r\n\r\n/, ''),
                        content: (spotText && item
                            .match(/Content-Type: .+\r\n\r\n/)[0]
                            .replace(/Content-Type: /, '')
                            .replace(/\r\n\r\n/, '')
                            .match(/text/)
                        ) ? item
                            .split(/\r\n\r\n/)[1]
                            .replace(/\r\n\r\n\r\n----/, '') : Buffer.from(item
                                .split(/\r\n\r\n/)[1]
                                .replace(/\r\n\r\n\r\n----/, ''), 'binary'),
                    };
                return result;
            }
            const result = {};
            result[
                item
                    .match(/name="[a-zA-Z_]+([a-zA-Z0-9_]*)"/)[0]
                    .split('=')[1]
                    .match(/[a-zA-Z_]+([a-zA-Z0-9_]*)/)[0]
            ] = item
                .split(/\r\n\r\n/)[1]
                .split(/\r\n--/)[0];
            return result;
        })
        .reduce((accumulator, current) => Object.assign(accumulator, current), {});
    return body;

--- a/index.js
+++ b/index.js
@@ -7,7 +7,7 @@
 module.exports.parse = (event, spotText) => {
     const boundary = getValueIgnoringKeyCase(event.headers, 'Content-Type').split('=')[1];
     const body = (event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('binary') : event.body)
-        .split(new RegExp(boundary))
+        .split(boundary)
         .filter(item => item.match(/Content-Disposition/))
         .map((item) => {
             if (item.match(/filename/)) {


Should an instance of AnythingLLM be hosted on an internal network and the attacked be explicitly granted a permission level of manager or admin, they could link-scrape internally resolving IPs of other services that are on the same network as AnythingLLM.

This would require the attacker also be able to guess these internal IPs as `/*` ranging is not possible, but could be brute forced.

There is a duty of care that other services on the same network would not be fully open and accessible via a simple CuRL with zero authentication as it is not possible to set headers or access via the link collector.

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

function validURL(url) {
  try {
    const destination = new URL(url);
    if (!VALID_PROTOCOLS.includes(destination.protocol)) return false;
    return true;
  } catch {}
  return false;
}

function validURL(url) {
  try {
    const destination = new URL(url);
    if (!VALID_PROTOCOLS.includes(destination.protocol)) return false;
    if (isInvalidIp(destination)) return false;
    return true;
  } catch {}
  return false;
}

const INVALID_OCTETS = [192, 172, 10, 127];

function isInvalidIp({ hostname }) {
  const IPRegex = new RegExp(
    /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gi
  );
  if (!IPRegex.test(hostname)) return false;
  const [octetOne, ..._rest] = hostname.split(".");

  // If fails to validate to number - abort and return as invalid.
  if (isNaN(Number(octetOne))) return true;
  return INVALID_OCTETS.includes(Number(octetOne));
}

--- a/collector/utils/url/index.js
+++ b/collector/utils/url/index.js
@@ -2,6 +2,7 @@
   try {
     const destination = new URL(url);
     if (!VALID_PROTOCOLS.includes(destination.protocol)) return false;
+    if (isInvalidIp(destination)) return false;
     return true;
   } catch {}
   return false;

--- /dev/null
+++ b/collector/utils/url/index.js
@@ -0,0 +1,13 @@
+const INVALID_OCTETS = [192, 172, 10, 127];
+
+function isInvalidIp({ hostname }) {
+  const IPRegex = new RegExp(
+    /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gi
+  );
+  if (!IPRegex.test(hostname)) return false;
+  const [octetOne, ..._rest] = hostname.split(".");
+
+  // If fails to validate to number - abort and return as invalid.
+  if (isNaN(Number(octetOne))) return true;
+  return INVALID_OCTETS.includes(Number(octetOne));
+}


The package workspace-tools before 0.18.4 are vulnerable to Command Injection via git argument injection. When calling the fetchRemoteBranch(remote: string, remoteBranch: string, cwd: string) function, both the remote and remoteBranch parameters are passed to the git fetch subcommand in a way that additional flags can be set. The additional flags can be used to perform a command injection.

Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')

The product constructs a string for a command to be executed by a separate component
in another control sphere, but it does not properly delimit the
intended arguments, options, or switches within that command string.

export function fetchRemote(remote: string, cwd: string) {
  const results = git(["fetch", remote], { cwd });

  if (!results.success) {
    throw gitError(`Cannot fetch remote: ${remote}`);
  }
}

export function fetchRemoteBranch(remote: string, remoteBranch: string, cwd: string) {
  const results = git(["fetch", remote, remoteBranch], { cwd });

  if (!results.success) {
    throw gitError(`Cannot fetch remote: ${remote} ${remoteBranch}`);
  }
}

export function fetchRemote(remote: string, cwd: string) {
  const results = git(["fetch", "--", remote], { cwd });

  if (!results.success) {
    throw gitError(`Cannot fetch remote: ${remote}`);
  }
}

export function fetchRemoteBranch(remote: string, remoteBranch: string, cwd: string) {
  const results = git(["fetch", "--", remote, remoteBranch], { cwd });

  if (!results.success) {
    throw gitError(`Cannot fetch remote: ${remote} ${remoteBranch}`);
  }
}

--- a/src/git.ts
+++ b/src/git.ts
@@ -1,5 +1,5 @@
 export function fetchRemote(remote: string, cwd: string) {
-  const results = git(["fetch", remote], { cwd });
+  const results = git(["fetch", "--", remote], { cwd });

   if (!results.success) {
     throw gitError(`Cannot fetch remote: ${remote}`);

--- a/src/git.ts
+++ b/src/git.ts
@@ -1,5 +1,5 @@
 export function fetchRemoteBranch(remote: string, remoteBranch: string, cwd: string) {
-  const results = git(["fetch", remote, remoteBranch], { cwd });
+  const results = git(["fetch", "--", remote, remoteBranch], { cwd });

   if (!results.success) {
     throw gitError(`Cannot fetch remote: ${remote} ${remoteBranch}`);


Just like in the previous report, an attacker could steal the account of different users. But in this case, it's a little bit more specific, because it is needed to be an editor in the same app as the victim.

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

  public async getThreads(appId: string, organizationId: string, appVersionsId: string): Promise<Thread[]> {
    return await this.threadRepository.find({
      where: {
        appId,
        organizationId,
        appVersionsId,
      },
    });
  }

  public async getThreads(appId: string, organizationId: string, appVersionsId: string): Promise<Thread[]> {
    return await createQueryBuilder(Thread, 'thread')
      .innerJoin('thread.user', 'user')
      .addSelect(['user.id', 'user.firstName', 'user.lastName'])
      .andWhere('thread.appId = :appId', {
        appId,
      })
      .andWhere('thread.organizationId = :organizationId', {
        organizationId,
      })
      .andWhere('thread.appVersionsId = :appVersionsId', {
        appVersionsId,
      })
      .getMany();
  }

--- a/server/src/services/thread.service.ts
+++ b/server/src/services/thread.service.ts
@@ -1,9 +1,15 @@
   public async getThreads(appId: string, organizationId: string, appVersionsId: string): Promise<Thread[]> {
-    return await this.threadRepository.find({
-      where: {
+    return await createQueryBuilder(Thread, 'thread')
+      .innerJoin('thread.user', 'user')
+      .addSelect(['user.id', 'user.firstName', 'user.lastName'])
+      .andWhere('thread.appId = :appId', {
         appId,
+      })
+      .andWhere('thread.organizationId = :organizationId', {
         organizationId,
+      })
+      .andWhere('thread.appVersionsId = :appVersionsId', {
         appVersionsId,
-      },
-    });
+      })
+      .getMany();
   }


safer-eval before 1.3.2 are vulnerable to Arbitrary Code Execution. A payload using constructor properties can escape the sandbox and execute arbitrary code.

  runInContext (code) {
    if (typeof code !== 'string') {
      throw new TypeError('not a string')
    }
    return vm.runInContext(
      '(function () {"use strict"; return ' + code + '})()',
      this._context,
      this._options
    )
  }

  runInContext (code) {
    if (typeof code !== 'string') {
      throw new TypeError('not a string')
    }
    let src = 'Object.constructor = function () {};\n'
    src += 'return ' + code + ';\n'

    return vm.runInContext(
      '(function () {"use strict"; ' + src + '})()',
      this._context,
      this._options
    )
  }

--- a/src/index.js
+++ b/src/index.js
@@ -2,8 +2,11 @@
     if (typeof code !== 'string') {
       throw new TypeError('not a string')
     }
+    let src = 'Object.constructor = function () {};\n'
+    src += 'return ' + code + ';\n'
+
     return vm.runInContext(
-      '(function () {"use strict"; return ' + code + '})()',
+      '(function () {"use strict"; ' + src + '})()',
       this._context,
       this._options
     )


EspoCRM is a free, open-source customer relationship management platform. Prior to version 9.0.8, HTML Injection in Knowledge Base (KB) articles leads to complete page defacement imitating the login page. Authenticated users with the read knowledge article privilege can browse to the KB article and if they submit their credentials, they get captured in plain text. The vulnerability is allowed by overly permissive HTML editing being allowed on the KB articles. Any authenticated user with the privilege to read KB articles is impacted. In an enterprise with multiple applications, the malicious KB article could be edited to match the login pages of other applications, which would make it useful for credential harvesting against other applications as well. Version 9.0.8 contains a patch for the issue.

Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')

The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.

    constructor() {
        this._registerHandlebarsHelpers();

        /** @private */
        this.mdBeforeList = [
            /*{
                regex: /```\n?([\s\S]*?)```/g,
                value: (s, string) => {
                    return '```\n' + string.replace(/\\\>/g, '>') + '```';
                },
            },*/
            {
                // Also covers triple-backtick blocks.
                regex: /`([\s\S]*?)`/g,
                value: (s, string) => {
                    // noinspection RegExpRedundantEscape
                    return '`' + string.replace(/\\\</g, '<') + '`';
                },
            },
        ];

        marked.setOptions({
            breaks: true,
            tables: false,
            headerIds: false,
        });

        DOMPurify.addHook('beforeSanitizeAttributes', function (node) {
            if (node instanceof HTMLAnchorElement) {
                if (node.getAttribute('target')) {
                    node.targetBlank = true;
                }
                else {
                    node.targetBlank = false;
                }
            }

            if (node instanceof HTMLOListElement && node.start && node.start > 99) {
                node.removeAttribute('start');
            }
        });

        DOMPurify.addHook('afterSanitizeAttributes', function (node) {
            if (node instanceof HTMLAnchorElement) {
                const href = node.getAttribute('href');

                if (href && !href.startsWith('#')) {
                    node.setAttribute('rel', 'noopener noreferrer');
                }

                if (node.targetBlank) {
                    node.setAttribute('target', '_blank');
                    node.setAttribute('rel', 'noopener noreferrer');
                }
            }
        });
    }

    constructor() {
        this._registerHandlebarsHelpers();

        /** @private */
        this.mdBeforeList = [
            /*{
                regex: /```\n?([\s\S]*?)```/g,
                value: (s, string) => {
                    return '```\n' + string.replace(/\\\>/g, '>') + '```';
                },
            },*/
            {
                // Also covers triple-backtick blocks.
                regex: /`([\s\S]*?)`/g,
                value: (s, string) => {
                    // noinspection RegExpRedundantEscape
                    return '`' + string.replace(/\\\</g, '<') + '`';
                },
            },
        ];

        marked.setOptions({
            breaks: true,
            tables: false,
            headerIds: false,
        });

        DOMPurify.addHook('beforeSanitizeAttributes', function (node) {
            if (node instanceof HTMLAnchorElement) {
                if (node.getAttribute('target')) {
                    node.targetBlank = true;
                }
                else {
                    node.targetBlank = false;
                }
            }

            if (node instanceof HTMLOListElement && node.start && node.start > 99) {
                node.removeAttribute('start');
            }

            if (node instanceof HTMLFormElement) {
                if (node.action) {
                    node.removeAttribute('action');
                }

                if (node.hasAttribute('method')) {
                    node.removeAttribute('method');
                }
            }

            if (node instanceof HTMLButtonElement) {
                if (node.type === 'submit') {
                    node.type = 'button';
                }
            }
        });

        DOMPurify.addHook('afterSanitizeAttributes', function (node) {
            if (node instanceof HTMLAnchorElement) {
                const href = node.getAttribute('href');

                if (href && !href.startsWith('#')) {
                    node.setAttribute('rel', 'noopener noreferrer');
                }

                if (node.targetBlank) {
                    node.setAttribute('target', '_blank');
                    node.setAttribute('rel', 'noopener noreferrer');
                }
            }
        });

        DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
            if (data.attrName === 'style') {
                const style = data.attrValue
                    .split(';')
                    .map(s => s.trim())
                    .filter(rule => {
                        const [property, value] = rule.split(':')
                            .map(s => s.trim().toLowerCase());

                        if (
                            property === 'position' &&
                            ['absolute', 'fixed', 'sticky'].includes(value)
                        ) {
                            return false;
                        }

                        return true;
                    });

                data.attrValue = style.join('; ');
            }
        });
    }

--- a/client/src/view-helper.js
+++ b/client/src/view-helper.js
@@ -38,6 +38,22 @@
             if (node instanceof HTMLOListElement && node.start && node.start > 99) {
                 node.removeAttribute('start');
             }
+
+            if (node instanceof HTMLFormElement) {
+                if (node.action) {
+                    node.removeAttribute('action');
+                }
+
+                if (node.hasAttribute('method')) {
+                    node.removeAttribute('method');
+                }
+            }
+
+            if (node instanceof HTMLButtonElement) {
+                if (node.type === 'submit') {
+                    node.type = 'button';
+                }
+            }
         });

         DOMPurify.addHook('afterSanitizeAttributes', function (node) {
@@ -54,4 +70,27 @@
                 }
             }
         });
+
+        DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
+            if (data.attrName === 'style') {
+                const style = data.attrValue
+                    .split(';')
+                    .map(s => s.trim())
+                    .filter(rule => {
+                        const [property, value] = rule.split(':')
+                            .map(s => s.trim().toLowerCase());
+
+                        if (
+                            property === 'position' &&
+                            ['absolute', 'fixed', 'sticky'].includes(value)
+                        ) {
+                            return false;
+                        }
+
+                        return true;
+                    });
+
+                data.attrValue = style.join('; ');
+            }
+        });
     }


OpenFGA is a high-performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. OpenFGA < v1.8.4 (Helm chart < openfga-0.2.22, docker < v.1.8.4) are vulnerable to authorization bypass when certain Check and ListObject calls are executed. Users on OpenFGA v1.8.4 or previous, specifically under the following conditions are affected by this authorization bypass vulnerability: 1. Calling Check API or ListObjects with a model that has a relation directly assignable to both public access AND userset with the same type. 2. A type bound public access tuple is assigned to an object. 3. userset tuple is not assigned to the same object. and 4. Check request's user field is a userset that has the same type as the type bound public access tuple's user type. Users are advised to upgrade to v1.8.5 which is backwards compatible. There are no known workarounds for this vulnerability.

Execution with Unnecessary Privileges

The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.

Improper Privilege Management

The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.

func shouldCheckPublicAssignable(ctx context.Context, reqTupleKey *openfgav1.TupleKey) bool {
	typesys, _ := typesystem.TypesystemFromContext(ctx)

	objectType := tuple.GetType(reqTupleKey.GetObject())
	relation := reqTupleKey.GetRelation()

	isPubliclyAssignable, _ := typesys.IsPubliclyAssignable(
		typesystem.DirectRelationReference(objectType, relation), // target
		tuple.GetType(reqTupleKey.GetUser()),
	)
	return isPubliclyAssignable
}

func shouldCheckPublicAssignable(ctx context.Context, reqTupleKey *openfgav1.TupleKey) bool {
	typesys, _ := typesystem.TypesystemFromContext(ctx)

	objectType := tuple.GetType(reqTupleKey.GetObject())
	relation := reqTupleKey.GetRelation()

	// if the user tuple is userset, by definition it cannot be a wildcard
	if tuple.IsObjectRelation(reqTupleKey.GetUser()) {
		return false
	}

	isPubliclyAssignable, _ := typesys.IsPubliclyAssignable(
		typesystem.DirectRelationReference(objectType, relation), // target
		tuple.GetType(reqTupleKey.GetUser()),
	)
	return isPubliclyAssignable
}

--- a/internal/graph/check.go
+++ b/internal/graph/check.go
@@ -3,6 +3,11 @@

 	objectType := tuple.GetType(reqTupleKey.GetObject())
 	relation := reqTupleKey.GetRelation()
+
+	// if the user tuple is userset, by definition it cannot be a wildcard
+	if tuple.IsObjectRelation(reqTupleKey.GetUser()) {
+		return false
+	}

 	isPubliclyAssignable, _ := typesys.IsPubliclyAssignable(
 		typesystem.DirectRelationReference(objectType, relation), // target


Casdoor before v1.126.1 was discovered to contain an arbitrary file deletion vulnerability via the uploadFile function.

func DeleteFile(provider *Provider, objectKey string) error {
	endpoint := getProviderEndpoint(provider)
	storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
	if storageProvider == nil {
		return fmt.Errorf("the provider type: %s is not supported", provider.Type)
	}

	if provider.Domain == "" {
		provider.Domain = storageProvider.GetEndpoint()
		UpdateProvider(provider.GetId(), provider)
	}

	return storageProvider.Delete(objectKey)
}

func DeleteFile(provider *Provider, objectKey string) error {
	// check fullFilePath is there security issue
	if strings.Contains(objectKey, "..") {
		return fmt.Errorf("the objectKey: %s is not allowed", objectKey)
	}

	endpoint := getProviderEndpoint(provider)
	storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
	if storageProvider == nil {
		return fmt.Errorf("the provider type: %s is not supported", provider.Type)
	}

	if provider.Domain == "" {
		provider.Domain = storageProvider.GetEndpoint()
		UpdateProvider(provider.GetId(), provider)
	}

	return storageProvider.Delete(objectKey)
}

--- a/object/storage.go
+++ b/object/storage.go
@@ -1,4 +1,9 @@
 func DeleteFile(provider *Provider, objectKey string) error {
+	// check fullFilePath is there security issue
+	if strings.Contains(objectKey, "..") {
+		return fmt.Errorf("the objectKey: %s is not allowed", objectKey)
+	}
+
 	endpoint := getProviderEndpoint(provider)
 	storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
 	if storageProvider == nil {


fastify-reply-from is an npm package which is a fastify plugin to forward the current http request to another server. In fastify-reply-from before version 4.0.2, by crafting a specific URL, it is possible to escape the prefix of the proxied backend service. If the base url of the proxied server is "/pub/", a user expect that accessing "/priv" on the target service would not be possible. In affected versions, it is possible. This is fixed in version 4.0.2.

function buildURL (source, reqBase) {
  const dest = new URL(source, reqBase)

  // if base is specified, source url should not override it
  if (reqBase && !reqBase.startsWith(dest.origin)) {
    throw new Error('source must be a relative path string')
  }

  return dest
}

function buildURL (source, reqBase) {
  const dest = new URL(source, reqBase)

  // if base is specified, source url should not override it
  if (reqBase) {
    if (!reqBase.endsWith('/') && dest.href.length > reqBase.length) {
      reqBase = reqBase + '/'
    }

    if (!dest.href.startsWith(reqBase)) {
      throw new Error('source must be a relative path string')
    }
  }

  return dest
}

--- a/lib/utils.js
+++ b/lib/utils.js
@@ -2,8 +2,14 @@
   const dest = new URL(source, reqBase)

   // if base is specified, source url should not override it
-  if (reqBase && !reqBase.startsWith(dest.origin)) {
-    throw new Error('source must be a relative path string')
+  if (reqBase) {
+    if (!reqBase.endsWith('/') && dest.href.length > reqBase.length) {
+      reqBase = reqBase + '/'
+    }
+
+    if (!dest.href.startsWith(reqBase)) {
+      throw new Error('source must be a relative path string')
+    }
   }

   return dest

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2021-21321:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/fastify-reply-from
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
npx tape test/build-url.js


In affected versions of dojox (NPM package), the jqMix method is vulnerable to Prototype Pollution. Prototype Pollution refers to the ability to inject properties into existing JavaScript language construct prototypes, such as objects. An attacker manipulates these attributes to overwrite, or pollute, a JavaScript application object prototype of the base object by injecting other values. This has been patched in versions 1.11.10, 1.12.8, 1.13.7, 1.14.6, 1.15.3 and 1.16.2

	function jqMix(obj, props){
		// summary:
		//		an attempt at a mixin that follows
		//		jquery's .extend rules. Seems odd. Not sure how
		//		to resolve this with dojo.mixin and what the use
		//		cases are for the jquery version.
		//		Copying some code from dojo._mixin.
		if(obj == props){
			return obj;
		}
		var tobj = {};
		for(var x in props){
			// the "tobj" condition avoid copying properties in "props"
			// inherited from Object.prototype.  For example, if obj has a custom
			// toString() method, don't overwrite it with the toString() method
			// that props inherited from Object.prototype
			if((tobj[x] === undefined || tobj[x] != props[x]) && props[x] !== undefined && obj != props[x]){
				if(dojo.isObject(obj[x]) && dojo.isObject(props[x])){
					if(dojo.isArray(props[x])){
						obj[x] = props[x];
					}else{
						obj[x] = jqMix(obj[x], props[x]);
					}
				}else{
					obj[x] = props[x];
				}
			}
		}
		// IE doesn't recognize custom toStrings in for..in
		if(dojo.isIE && props){
			var p = props.toString;
			if(typeof p == "function" && p != obj.toString && p != tobj.toString &&
				p != "\nfunction toString() {\n    [native code]\n}\n"){
					obj.toString = props.toString;
			}
		}
		return obj; // Object
	}

	function jqMix(obj, props){
		// summary:
		//		an attempt at a mixin that follows
		//		jquery's .extend rules. Seems odd. Not sure how
		//		to resolve this with dojo.mixin and what the use
		//		cases are for the jquery version.
		//		Copying some code from dojo._mixin.
		if(obj == props){
			return obj;
		}
		var tobj = {};
		for(var x in props){
			// the "tobj" condition avoid copying properties in "props"
			// inherited from Object.prototype.  For example, if obj has a custom
			// toString() method, don't overwrite it with the toString() method
			// that props inherited from Object.prototype
			if(x !== '__proto__ ' && ((tobj[x] === undefined || tobj[x] != props[x])) && props[x] !== undefined && obj != props[x]){
				if(dojo.isObject(obj[x]) && dojo.isObject(props[x])){
					if(dojo.isArray(props[x])){
						obj[x] = props[x];
					}else{
						obj[x] = jqMix(obj[x], props[x]);
					}
				}else{
					obj[x] = props[x];
				}
			}
		}
		// IE doesn't recognize custom toStrings in for..in
		if(dojo.isIE && props){
			var p = props.toString;
			if(typeof p == "function" && p != obj.toString && p != tobj.toString &&
				p != "\nfunction toString() {\n    [native code]\n}\n"){
					obj.toString = props.toString;
			}
		}
		return obj; // Object
	}

--- a/jq.js
+++ b/jq.js
@@ -14,7 +14,7 @@
 			// inherited from Object.prototype.  For example, if obj has a custom
 			// toString() method, don't overwrite it with the toString() method
 			// that props inherited from Object.prototype
-			if((tobj[x] === undefined || tobj[x] != props[x]) && props[x] !== undefined && obj != props[x]){
+			if(x !== '__proto__ ' && ((tobj[x] === undefined || tobj[x] != props[x])) && props[x] !== undefined && obj != props[x]){
 				if(dojo.isObject(obj[x]) && dojo.isObject(props[x])){
 					if(dojo.isArray(props[x])){
 						obj[x] = props[x];


A path traversal vulnerability exists in the latest version of gaizhenbiao/chuanhuchatgpt. The vulnerability arises from unsanitized input handling in multiple features, including user upload, directory creation, and template loading. Specifically, the load_chat_history function in modules/models/base_model.py allows arbitrary file uploads, potentially leading to remote code execution (RCE). The get_history_names function in utils.py permits arbitrary directory creation. Additionally, the load_template function in utils.py can be exploited to leak the first column of CSV files. These issues stem from improper sanitization of user inputs concatenated with directory paths using os.path.join.

    def load_chat_history(self, new_history_file_path=None):
        logging.debug(f"{self.user_name} \u52a0\u8f7d\u5bf9\u8bdd\u5386\u53f2\u4e2d\u2026\u2026")
        if new_history_file_path is not None:
            if type(new_history_file_path) != str:
                # copy file from new_history_file_path.name to os.path.join(HISTORY_DIR, self.user_name)
                new_history_file_path = new_history_file_path.name
                shutil.copyfile(
                    new_history_file_path,
                    os.path.join(
                        HISTORY_DIR,
                        self.user_name,
                        os.path.basename(new_history_file_path),
                    ),
                )
                self.history_file_path = os.path.basename(new_history_file_path)
            else:
                self.history_file_path = new_history_file_path
        try:
            if self.history_file_path == os.path.basename(self.history_file_path):
                history_file_path = os.path.join(
                    HISTORY_DIR, self.user_name, self.history_file_path
                )
            else:
                history_file_path = self.history_file_path
            if not self.history_file_path.endswith(".json"):
                history_file_path += ".json"
            with open(history_file_path, "r", encoding="utf-8") as f:
                saved_json = json.load(f)
            try:
                if type(saved_json["history"][0]) == str:
                    logging.info("\u5386\u53f2\u8bb0\u5f55\u683c\u5f0f\u4e3a\u65e7\u7248\uff0c\u6b63\u5728\u8f6c\u6362\u2026\u2026")
                    new_history = []
                    for index, item in enumerate(saved_json["history"]):
                        if index % 2 == 0:
                            new_history.append(construct_user(item))
                        else:
                            new_history.append(construct_assistant(item))
                    saved_json["history"] = new_history
                    logging.info(new_history)
            except:
                pass
            if len(saved_json["chatbot"]) < len(saved_json["history"]) // 2:
                logging.info("Trimming corrupted history...")
                saved_json["history"] = saved_json["history"][
                    -len(saved_json["chatbot"]) :
                ]
                logging.info(f"Trimmed history: {saved_json['history']}")
            # Sanitize chatbot
            saved_json["chatbot"] = remove_html_tags(saved_json["chatbot"])
            logging.debug(f"{self.user_name} \u52a0\u8f7d\u5bf9\u8bdd\u5386\u53f2\u5b8c\u6bd5")
            self.history = saved_json["history"]
            self.single_turn = saved_json.get("single_turn", self.single_turn)
            self.temperature = saved_json.get("temperature", self.temperature)
            self.top_p = saved_json.get("top_p", self.top_p)
            self.n_choices = saved_json.get("n_choices", self.n_choices)
            self.stop_sequence = list(saved_json.get("stop_sequence", self.stop_sequence))
            self.token_upper_limit = saved_json.get(
                "token_upper_limit", self.token_upper_limit
            )
            self.max_generation_token = saved_json.get(
                "max_generation_token", self.max_generation_token
            )
            self.presence_penalty = saved_json.get(
                "presence_penalty", self.presence_penalty
            )
            self.frequency_penalty = saved_json.get(
                "frequency_penalty", self.frequency_penalty
            )
            self.logit_bias = saved_json.get("logit_bias", self.logit_bias)
            self.user_identifier = saved_json.get("user_identifier", self.user_name)
            self.metadata = saved_json.get("metadata", self.metadata)
            self.stream = saved_json.get("stream", self.stream)
            self.chatbot = saved_json["chatbot"]
            return (
                os.path.basename(self.history_file_path)[:-5],
                saved_json["system"],
                gr.update(value=saved_json["chatbot"]),
                self.single_turn,
                self.temperature,
                self.top_p,
                self.n_choices,
                ",".join(self.stop_sequence),
                self.token_upper_limit,
                self.max_generation_token,
                self.presence_penalty,
                self.frequency_penalty,
                self.logit_bias,
                self.user_identifier,
                self.stream
            )
        except:
            # \u6ca1\u6709\u5bf9\u8bdd\u5386\u53f2\u6216\u8005\u5bf9\u8bdd\u5386\u53f2\u89e3\u6790\u5931\u8d25
            logging.debug(f"\u6ca1\u6709\u627e\u5230\u5bf9\u8bdd\u5386\u53f2\u8bb0\u5f55 {self.history_file_path}")
            self.reset()
            return (
                os.path.basename(self.history_file_path),
                self.system_prompt,
                gr.update(value=[]),
                self.single_turn,
                self.temperature,
                self.top_p,
                self.n_choices,
                ",".join(self.stop_sequence),
                self.token_upper_limit,
                self.max_generation_token,
                self.presence_penalty,
                self.frequency_penalty,
                self.logit_bias,
                self.user_identifier,
                self.stream
            )

    def load_chat_history(self, new_history_file_path=None):
        logging.debug(f"{self.user_name} \u52a0\u8f7d\u5bf9\u8bdd\u5386\u53f2\u4e2d\u2026\u2026")
        if new_history_file_path is not None:
            if type(new_history_file_path) != str:
                # copy file from new_history_file_path.name to os.path.join(HISTORY_DIR, self.user_name)
                new_history_file_path = new_history_file_path.name
                target_path = os.path.join(HISTORY_DIR, self.user_name, new_history_file_path)
                # Check if the file is in the history directory
                assert os.path.realpath(new_history_file_path).startswith(os.path.realpath(HISTORY_DIR))
                assert os.path.realpath(target_path).startswith(os.path.realpath(HISTORY_DIR))
                assert self.user_name in [i[0] for i in auth_list]
                shutil.copyfile(
                    new_history_file_path,
                    os.path.join(
                        HISTORY_DIR,
                        self.user_name,
                        os.path.basename(new_history_file_path),
                    ),
                )
                self.history_file_path = os.path.basename(new_history_file_path)
            else:
                self.history_file_path = new_history_file_path
        try:
            if self.history_file_path == os.path.basename(self.history_file_path):
                history_file_path = os.path.join(
                    HISTORY_DIR, self.user_name, self.history_file_path
                )
            else:
                history_file_path = self.history_file_path
            if not self.history_file_path.endswith(".json"):
                history_file_path += ".json"
            with open(history_file_path, "r", encoding="utf-8") as f:
                saved_json = json.load(f)
            try:
                if type(saved_json["history"][0]) == str:
                    logging.info("\u5386\u53f2\u8bb0\u5f55\u683c\u5f0f\u4e3a\u65e7\u7248\uff0c\u6b63\u5728\u8f6c\u6362\u2026\u2026")
                    new_history = []
                    for index, item in enumerate(saved_json["history"]):
                        if index % 2 == 0:
                            new_history.append(construct_user(item))
                        else:
                            new_history.append(construct_assistant(item))
                    saved_json["history"] = new_history
                    logging.info(new_history)
            except:
                pass
            if len(saved_json["chatbot"]) < len(saved_json["history"]) // 2:
                logging.info("Trimming corrupted history...")
                saved_json["history"] = saved_json["history"][
                    -len(saved_json["chatbot"]) :
                ]
                logging.info(f"Trimmed history: {saved_json['history']}")
            # Sanitize chatbot
            saved_json["chatbot"] = remove_html_tags(saved_json["chatbot"])
            logging.debug(f"{self.user_name} \u52a0\u8f7d\u5bf9\u8bdd\u5386\u53f2\u5b8c\u6bd5")
            self.history = saved_json["history"]
            self.single_turn = saved_json.get("single_turn", self.single_turn)
            self.temperature = saved_json.get("temperature", self.temperature)
            self.top_p = saved_json.get("top_p", self.top_p)
            self.n_choices = saved_json.get("n_choices", self.n_choices)
            self.stop_sequence = list(saved_json.get("stop_sequence", self.stop_sequence))
            self.token_upper_limit = saved_json.get(
                "token_upper_limit", self.token_upper_limit
            )
            self.max_generation_token = saved_json.get(
                "max_generation_token", self.max_generation_token
            )
            self.presence_penalty = saved_json.get(
                "presence_penalty", self.presence_penalty
            )
            self.frequency_penalty = saved_json.get(
                "frequency_penalty", self.frequency_penalty
            )
            self.logit_bias = saved_json.get("logit_bias", self.logit_bias)
            self.user_identifier = saved_json.get("user_identifier", self.user_name)
            self.metadata = saved_json.get("metadata", self.metadata)
            self.stream = saved_json.get("stream", self.stream)
            self.chatbot = saved_json["chatbot"]
            return (
                os.path.basename(self.history_file_path)[:-5],
                saved_json["system"],
                gr.update(value=saved_json["chatbot"]),
                self.single_turn,
                self.temperature,
                self.top_p,
                self.n_choices,
                ",".join(self.stop_sequence),
                self.token_upper_limit,
                self.max_generation_token,
                self.presence_penalty,
                self.frequency_penalty,
                self.logit_bias,
                self.user_identifier,
                self.stream
            )
        except:
            # \u6ca1\u6709\u5bf9\u8bdd\u5386\u53f2\u6216\u8005\u5bf9\u8bdd\u5386\u53f2\u89e3\u6790\u5931\u8d25
            logging.debug(f"\u6ca1\u6709\u627e\u5230\u5bf9\u8bdd\u5386\u53f2\u8bb0\u5f55 {self.history_file_path}")
            self.reset()
            return (
                os.path.basename(self.history_file_path),
                self.system_prompt,
                gr.update(value=[]),
                self.single_turn,
                self.temperature,
                self.top_p,
                self.n_choices,
                ",".join(self.stop_sequence),
                self.token_upper_limit,
                self.max_generation_token,
                self.presence_penalty,
                self.frequency_penalty,
                self.logit_bias,
                self.user_identifier,
                self.stream
            )

--- a/modules/models/base_model.py
+++ b/modules/models/base_model.py
@@ -4,6 +4,11 @@
             if type(new_history_file_path) != str:
                 # copy file from new_history_file_path.name to os.path.join(HISTORY_DIR, self.user_name)
                 new_history_file_path = new_history_file_path.name
+                target_path = os.path.join(HISTORY_DIR, self.user_name, new_history_file_path)
+                # Check if the file is in the history directory
+                assert os.path.realpath(new_history_file_path).startswith(os.path.realpath(HISTORY_DIR))
+                assert os.path.realpath(target_path).startswith(os.path.realpath(HISTORY_DIR))
+                assert self.user_name in [i[0] for i in auth_list]
                 shutil.copyfile(
                     new_history_file_path,
                     os.path.join(


Zulip Server 1.5.1 and below suffer from an error in the implementation of the invite_by_admins_only setting in the Zulip group chat application server that allowed an authenticated user to invite other users to join a Zulip organization even if the organization was configured to prevent this.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

def json_invite_users(request, user_profile,
                      invitee_emails_raw=REQ("invitee_emails"),
                      body=REQ("custom_body", default=None)):
    # type: (HttpRequest, UserProfile, str, Optional[str]) -> HttpResponse
    if not invitee_emails_raw:
        return json_error(_("You must specify at least one email address."))
    if body == '':
        body = None

    invitee_emails = get_invitee_emails_set(invitee_emails_raw)

    stream_names = request.POST.getlist('stream')
    if not stream_names:
        return json_error(_("You must specify at least one stream for invitees to join."))

    # We unconditionally sub you to the notifications stream if it
    # exists and is public.
    notifications_stream = user_profile.realm.notifications_stream  # type: Optional[Stream]
    if notifications_stream and not notifications_stream.invite_only:
        stream_names.append(notifications_stream.name)

    streams = []  # type: List[Stream]
    for stream_name in stream_names:
        try:
            (stream, recipient, sub) = access_stream_by_name(user_profile, stream_name)
        except JsonableError:
            return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,))
        streams.append(stream)

    ret_error, error_data = do_invite_users(user_profile, invitee_emails, streams, body)

    if ret_error is not None:
        return json_error(data=error_data, msg=ret_error)
    else:
        return json_success()

def json_invite_users(request, user_profile,
                      invitee_emails_raw=REQ("invitee_emails"),
                      body=REQ("custom_body", default=None)):
    # type: (HttpRequest, UserProfile, str, Optional[str]) -> HttpResponse
    if user_profile.realm.invite_by_admins_only and not user_profile.is_realm_admin:
        return json_error(_("Must be a realm administrator"))
    if not invitee_emails_raw:
        return json_error(_("You must specify at least one email address."))
    if body == '':
        body = None

    invitee_emails = get_invitee_emails_set(invitee_emails_raw)

    stream_names = request.POST.getlist('stream')
    if not stream_names:
        return json_error(_("You must specify at least one stream for invitees to join."))

    # We unconditionally sub you to the notifications stream if it
    # exists and is public.
    notifications_stream = user_profile.realm.notifications_stream  # type: Optional[Stream]
    if notifications_stream and not notifications_stream.invite_only:
        stream_names.append(notifications_stream.name)

    streams = []  # type: List[Stream]
    for stream_name in stream_names:
        try:
            (stream, recipient, sub) = access_stream_by_name(user_profile, stream_name)
        except JsonableError:
            return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,))
        streams.append(stream)

    ret_error, error_data = do_invite_users(user_profile, invitee_emails, streams, body)

    if ret_error is not None:
        return json_error(data=error_data, msg=ret_error)
    else:
        return json_success()

--- a/zerver/views/invite.py
+++ b/zerver/views/invite.py
@@ -2,6 +2,8 @@
                       invitee_emails_raw=REQ("invitee_emails"),
                       body=REQ("custom_body", default=None)):
     # type: (HttpRequest, UserProfile, str, Optional[str]) -> HttpResponse
+    if user_profile.realm.invite_by_admins_only and not user_profile.is_realm_admin:
+        return json_error(_("Must be a realm administrator"))
     if not invitee_emails_raw:
         return json_error(_("You must specify at least one email address."))
     if body == '':


This affects the package portprocesses before 1.0.5. If (attacker-controlled) user input is given to the killProcess function, it is possible for an attacker to execute arbitrary commands. This is due to use of the child_process exec function without input sanitization.

async function task() {
	const argv = [...process.argv];
	const port = process.argv.pop();
	const command = process.argv.pop();

	switch (command) {
		case "kill":
			const result = await main.killAllProcessesOnPort(port);
			console.log(result.filter(item => !item.success).map(item => `Failed to kill process ${item.pid}`).join('\n'));
			break;
		case "list":
			const result = await main.listProcessesOnPort(port);
			console.log(result);
			break;
		default:
			console.error("Command not found");
	}
}

const listProcessesOnPort = module.exports.listProcessesOnPort = async port => {
	try {
		const result = (await exec(`lsof -i :${port}`)).output.split('\n');
		const headers = result.shift().split(' ').filter(item => !!item.trim() && item.trim() !== "").map(item => item.toLowerCase());
		return result.filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue) => {
			accumulator.push(currentValue.split(' ').filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue, index) => {
				if (index > headers.length - 1) {
					accumulator[headers[headers.length - 1]] = (!!accumulator[headers[headers.length - 1]].trim() && accumulator[headers[headers.length - 1]].trim() !== "") ? `${accumulator[headers[headers.length - 1]]} ${currentValue}` : currentValue;
				} else {
					accumulator[headers[index]] = currentValue;
				}
				return accumulator;
			}, {}));
			return accumulator;
		}, []);
	} catch (e) {
		console.error(e);
	}
};

const killProcess = module.exports.killProcess = async pid => {
	try {
		await exec(`kill ${pid}`);
		return true;
	} catch (e) {
		return false;
	}
};

async function task() {
	const argv = [...process.argv];
	const port = process.argv.pop();
	const command = process.argv.pop();

	let result;
	switch (command) {
		case "kill":
			result = await main.killAllProcessesOnPort(port);
			console.log(result.filter(item => !item.success).map(item => `Failed to kill process ${item.pid}`).join('\n'));
			break;
		case "list":
			result = await main.listProcessesOnPort(port);
			console.log(result);
			break;
		default:
			console.error("Command not found");
	}
}

const listProcessesOnPort = module.exports.listProcessesOnPort = async port => {
	const portNumber = parseInt(port, 10);
	if (Number.isNaN(portNumber)) {
		console.error("Must provide number for port.");
		return;
	}
	try {
		const result = (await exec(`lsof -i :${portNumber}`)).output.split('\n');
		const headers = result.shift().split(' ').filter(item => !!item.trim() && item.trim() !== "").map(item => item.toLowerCase());
		return result.filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue) => {
			accumulator.push(currentValue.split(' ').filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue, index) => {
				if (index > headers.length - 1) {
					accumulator[headers[headers.length - 1]] = (!!accumulator[headers[headers.length - 1]].trim() && accumulator[headers[headers.length - 1]].trim() !== "") ? `${accumulator[headers[headers.length - 1]]} ${currentValue}` : currentValue;
				} else {
					accumulator[headers[index]] = currentValue;
				}
				return accumulator;
			}, {}));
			return accumulator;
		}, []);
	} catch (e) {
		console.error(e);
	}
};

const killProcess = module.exports.killProcess = async pid => {
	const pidNumber = parseInt(pid, 10);
	if (Number.isNaN(pidNumber)) {
		console.error("Must provide number for process identifier.");
		return false;
	}

	try {
		await exec(`kill ${pidNumber}`);
		return true;
	} catch (e) {
		return false;
	}
};

--- a/cli.js
+++ b/cli.js
@@ -3,13 +3,14 @@
 	const port = process.argv.pop();
 	const command = process.argv.pop();

+	let result;
 	switch (command) {
 		case "kill":
-			const result = await main.killAllProcessesOnPort(port);
+			result = await main.killAllProcessesOnPort(port);
 			console.log(result.filter(item => !item.success).map(item => `Failed to kill process ${item.pid}`).join('\n'));
 			break;
 		case "list":
-			const result = await main.listProcessesOnPort(port);
+			result = await main.listProcessesOnPort(port);
 			console.log(result);
 			break;
 		default:

--- a/index.js
+++ b/index.js
@@ -1,6 +1,11 @@
 const listProcessesOnPort = module.exports.listProcessesOnPort = async port => {
+	const portNumber = parseInt(port, 10);
+	if (Number.isNaN(portNumber)) {
+		console.error("Must provide number for port.");
+		return;
+	}
 	try {
-		const result = (await exec(`lsof -i :${port}`)).output.split('\n');
+		const result = (await exec(`lsof -i :${portNumber}`)).output.split('\n');
 		const headers = result.shift().split(' ').filter(item => !!item.trim() && item.trim() !== "").map(item => item.toLowerCase());
 		return result.filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue) => {
 			accumulator.push(currentValue.split(' ').filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue, index) => {

--- a/index.js
+++ b/index.js
@@ -1,6 +1,12 @@
 const killProcess = module.exports.killProcess = async pid => {
+	const pidNumber = parseInt(pid, 10);
+	if (Number.isNaN(pidNumber)) {
+		console.error("Must provide number for process identifier.");
+		return false;
+	}
+
 	try {
-		await exec(`kill ${pid}`);
+		await exec(`kill ${pidNumber}`);
 		return true;
 	} catch (e) {
 		return false;


pretalx 2.3.1 before 2.3.2 allows path traversal in HTML export (a non-default feature). Users were able to upload crafted HTML documents that trigger the reading of arbitrary files.

def dump_content(destination, path, getter):
    logging.debug(path)
    content = getter(path)
    if path.endswith("/"):
        path = path + "index.html"

    path = Path(destination) / path.lstrip("/")
    path.parent.mkdir(parents=True, exist_ok=True)

    with open(path, "wb") as f:
        f.write(content)
    return content

def get_mediastatic_content(url):
    if url.startswith(settings.STATIC_URL):
        local_path = settings.STATIC_ROOT / url[len(settings.STATIC_URL) :]
    elif url.startswith(settings.MEDIA_URL):
        local_path = settings.MEDIA_ROOT / url[len(settings.MEDIA_URL) :]
    else:
        raise FileNotFoundError()

    with open(local_path, "rb") as f:
        return f.read()

def dump_content(destination, path, getter):
    logging.debug(path)
    content = getter(path)
    if path.endswith("/"):
        path = path + "index.html"

    path = (Path(destination) / path.lstrip("/")).resolve()
    if not Path(destination) in path.parents:
        raise CommandError("Path traversal detected, aborting.")
    path.parent.mkdir(parents=True, exist_ok=True)

    with open(path, "wb") as f:
        f.write(content)
    return content

def get_mediastatic_content(url):
    if url.startswith(settings.STATIC_URL):
        local_path = settings.STATIC_ROOT / url[len(settings.STATIC_URL) :]
    elif url.startswith(settings.MEDIA_URL):
        local_path = settings.MEDIA_ROOT / url[len(settings.MEDIA_URL) :]
    else:
        raise FileNotFoundError()

    # Prevent directory traversal, make sure the path is inside the media or static root
    local_path = local_path.resolve(strict=True)
    if not any(
        path in local_path.parents
        for path in (settings.MEDIA_ROOT, settings.STATIC_ROOT)
    ):
        raise FileNotFoundError()

    with open(local_path, "rb") as f:
        return f.read()

--- a/src/pretalx/agenda/management/commands/export_schedule_html.py
+++ b/src/pretalx/agenda/management/commands/export_schedule_html.py
@@ -4,7 +4,9 @@
     if path.endswith("/"):
         path = path + "index.html"

-    path = Path(destination) / path.lstrip("/")
+    path = (Path(destination) / path.lstrip("/")).resolve()
+    if not Path(destination) in path.parents:
+        raise CommandError("Path traversal detected, aborting.")
     path.parent.mkdir(parents=True, exist_ok=True)

     with open(path, "wb") as f:

--- a/src/pretalx/agenda/management/commands/export_schedule_html.py
+++ b/src/pretalx/agenda/management/commands/export_schedule_html.py
@@ -6,5 +6,13 @@
     else:
         raise FileNotFoundError()

+    # Prevent directory traversal, make sure the path is inside the media or static root
+    local_path = local_path.resolve(strict=True)
+    if not any(
+        path in local_path.parents
+        for path in (settings.MEDIA_ROOT, settings.STATIC_ROOT)
+    ):
+        raise FileNotFoundError()
+
     with open(local_path, "rb") as f:
         return f.read()


Nodebb is an open source Node.js based forum software. In affected versions incorrect logic present in the token verification step unintentionally allowed master token access to the API. The vulnerability has been patch as of v1.18.5. Users are advised to upgrade as soon as possible.

Improper Authentication

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.

Auth.verifyToken = async function (token, done) {
	let { tokens = [] } = await meta.settings.get('core.api');
	tokens = tokens.reduce((memo, cur) => {
		memo[cur.token] = cur.uid;
		return memo;
	}, {});

	const uid = tokens[token];

	if (uid !== undefined) {
		if (parseInt(uid, 10) > 0) {
			done(null, {
				uid: uid,
			});
		} else {
			done(null, {
				master: true,
			});
		}
	} else {
		done(false);
	}
};

Auth.verifyToken = async function (token, done) {
	const { tokens = [] } = await meta.settings.get('core.api');
	const tokenObj = tokens.find(t => t.token === token);
	const uid = tokenObj ? tokenObj.uid : undefined;

	if (uid !== undefined) {
		if (parseInt(uid, 10) > 0) {
			done(null, {
				uid: uid,
			});
		} else {
			done(null, {
				master: true,
			});
		}
	} else {
		done(false);
	}
};

--- a/src/routes/authentication.js
+++ b/src/routes/authentication.js
@@ -1,11 +1,7 @@
 Auth.verifyToken = async function (token, done) {
-	let { tokens = [] } = await meta.settings.get('core.api');
-	tokens = tokens.reduce((memo, cur) => {
-		memo[cur.token] = cur.uid;
-		return memo;
-	}, {});
-
-	const uid = tokens[token];
+	const { tokens = [] } = await meta.settings.get('core.api');
+	const tokenObj = tokens.find(t => t.token === token);
+	const uid = tokenObj ? tokenObj.uid : undefined;

 	if (uid !== undefined) {
 		if (parseInt(uid, 10) > 0) {


PaddlePaddle before 2.6.0 has a command injection in get_online_pass_interval. This resulted in the ability to execute arbitrary commands on the operating system.

    def get_online_pass_interval(
        self, days, hours, split_interval, split_per_pass, is_data_hourly_placed
    ):
        """
        get online pass interval

        Args:
            days(str): days to train
            hours(str): hours to train
            split_interval(int|str): split interval
            split_per_pass(int}str): split per pass
            is_data_hourly_placed(bool): is data hourly placed

        Returns:
            online_pass_interval(list)

        Examples:
            .. code-block:: python

                >>> # doctest: +REQUIRES(env:DISTRIBUTED)
                >>> from paddle.incubate.distributed.fleet.fleet_util import FleetUtil
                >>> fleet_util = FleetUtil()
                >>> online_pass_interval = fleet_util.get_online_pass_interval(
                ...     days="{20190720..20190729}",
                ...     hours="{0..23}",
                ...     split_interval=5,
                ...     split_per_pass=2,
                ...     is_data_hourly_placed=False)

        """
        days = os.popen("echo -n " + days).read().split(" ")
        hours = os.popen("echo -n " + hours).read().split(" ")
        split_interval = int(split_interval)
        split_per_pass = int(split_per_pass)
        splits_per_day = (
            (int(hours[-1]) - int(hours[0]) + 1) * 60 // split_interval
        )
        pass_per_day = splits_per_day // split_per_pass
        left_train_hour = int(hours[0])
        right_train_hour = int(hours[-1])

        start = 0
        split_path = []
        for i in range(splits_per_day):
            h = start // 60
            m = start % 60
            if h < left_train_hour or h > right_train_hour:
                start += split_interval
                continue
            if is_data_hourly_placed:
                split_path.append("%02d" % h)
            else:
                split_path.append("%02d%02d" % (h, m))
            start += split_interval

        start = 0
        online_pass_interval = []
        for i in range(pass_per_day):
            online_pass_interval.append([])
            for j in range(start, start + split_per_pass):
                online_pass_interval[i].append(split_path[j])
            start += split_per_pass

        return online_pass_interval

    def get_online_pass_interval(
        self, days, hours, split_interval, split_per_pass, is_data_hourly_placed
    ):
        """
        get online pass interval

        Args:
            days(str): days to train
            hours(str): hours to train
            split_interval(int|str): split interval
            split_per_pass(int}str): split per pass
            is_data_hourly_placed(bool): is data hourly placed

        Returns:
            online_pass_interval(list)

        Examples:
            .. code-block:: python

                >>> # doctest: +REQUIRES(env:DISTRIBUTED)
                >>> from paddle.incubate.distributed.fleet.fleet_util import FleetUtil
                >>> fleet_util = FleetUtil()
                >>> online_pass_interval = fleet_util.get_online_pass_interval(
                ...     days="{20190720..20190729}",
                ...     hours="{0..23}",
                ...     split_interval=5,
                ...     split_per_pass=2,
                ...     is_data_hourly_placed=False)

        """
        assert (
            "|" not in days
            and ";" not in days
            and "\\" not in days
            and "/" not in days
            and "(" not in days
            and ")" not in days
        ), r"days should not contain [|,;,\,/,(,)]"
        days = os.popen("echo -n " + days).read().split(" ")
        assert (
            "|" not in hours
            and ";" not in hours
            and "\\" not in hours
            and "/" not in hours
            and "(" not in hours
            and ")" not in days
        ), r"hours should not contain [|,;,\,/,(,)]"
        hours = os.popen("echo -n " + hours).read().split(" ")
        split_interval = int(split_interval)
        split_per_pass = int(split_per_pass)
        splits_per_day = (
            (int(hours[-1]) - int(hours[0]) + 1) * 60 // split_interval
        )
        pass_per_day = splits_per_day // split_per_pass
        left_train_hour = int(hours[0])
        right_train_hour = int(hours[-1])

        start = 0
        split_path = []
        for i in range(splits_per_day):
            h = start // 60
            m = start % 60
            if h < left_train_hour or h > right_train_hour:
                start += split_interval
                continue
            if is_data_hourly_placed:
                split_path.append("%02d" % h)
            else:
                split_path.append("%02d%02d" % (h, m))
            start += split_interval

        start = 0
        online_pass_interval = []
        for i in range(pass_per_day):
            online_pass_interval.append([])
            for j in range(start, start + split_per_pass):
                online_pass_interval[i].append(split_path[j])
            start += split_per_pass

        return online_pass_interval

--- a/python/paddle/incubate/distributed/fleet/fleet_util.py
+++ b/python/paddle/incubate/distributed/fleet/fleet_util.py
@@ -28,7 +28,23 @@
                 ...     is_data_hourly_placed=False)

         """
+        assert (
+            "|" not in days
+            and ";" not in days
+            and "\\" not in days
+            and "/" not in days
+            and "(" not in days
+            and ")" not in days
+        ), r"days should not contain [|,;,\,/,(,)]"
         days = os.popen("echo -n " + days).read().split(" ")
+        assert (
+            "|" not in hours
+            and ";" not in hours
+            and "\\" not in hours
+            and "/" not in hours
+            and "(" not in hours
+            and ")" not in days
+        ), r"hours should not contain [|,;,\,/,(,)]"
         hours = os.popen("echo -n " + hours).read().split(" ")
         split_interval = int(split_interval)
         split_per_pass = int(split_per_pass)


pyLoad is a free and open-source Download Manager. The folder `/.pyload/scripts` has scripts which are run when certain actions are completed, for e.g. a download is finished. By downloading a executable file to a folder in /scripts and performing the respective action, remote code execution can be achieved in versions prior to 0.5.0b3.dev87. A file can be downloaded to such a folder by changing the download folder to a folder in `/scripts` path and using the `/flashgot` API to download the file. This vulnerability allows an attacker with access to change the settings on a pyload server to execute arbitrary code and completely compromise the system. Version 0.5.0b3.dev87 fixes this issue.

    def _init_config(self, userdir, tempdir, storagedir, debug):
        from .config.parser import ConfigParser

        self.userdir = os.path.realpath(userdir)
        self.tempdir = os.path.realpath(tempdir)
        os.makedirs(self.userdir, exist_ok=True)
        os.makedirs(self.tempdir, exist_ok=True)

        self.config = ConfigParser(self.userdir)

        if debug is None:
            if self.config.get("general", "debug_mode"):
                debug_level = self.config.get("general", "debug_level")
                self._debug = self.DEBUG_LEVEL_MAP[debug_level]
        else:
            self._debug = max(0, int(debug))

        # If no argument set, read storage dir from config file,
        # otherwise save setting to config dir
        if storagedir is None:
            storagedir = self.config.get("general", "storage_folder")
            # Make sure storage_folder is not empty
            if not storagedir:
                self.config.set("general", "storage_folder", "~/Downloads/pyLoad")
                storagedir = self.config.get("general", "storage_folder")
        else:
            self.config.set("general", "storage_folder", storagedir)
        os.makedirs(storagedir, exist_ok=True)

        if not self._dry_run:
            self.config.save()  #: save so config files gets filled

    def set_config_value(self, category, option, value, section="core"):
        """
        Set new config value.

        :param category:
        :param option:
        :param value: new config value
        :param section: 'plugin' or 'core
        """
        self.pyload.addon_manager.dispatch_event(
            "config_changed", category, option, value, section
        )

        if section == "core":
            self.pyload.config[category][option] = value

            if option in (
                "limit_speed",
                "max_speed",
            ):  #: not so nice to update the limit
                self.pyload.request_factory.update_bucket()

        elif section == "plugin":
            self.pyload.config.set_plugin(category, option, value)

def save_config():
    api = flask.current_app.config["PYLOAD_API"]
    category = flask.request.args.get('category')
    if category not in ("core", "plugin"):
        return jsonify(False), 500

    for key, value in flask.request.form.items():
        try:
            section, option = key.split("|")
        except Exception:
            continue

        if section == 'general' and option=='storage_folder':
            abs_path_value = os.path.join(os.path.abspath(value).lower(), "")
            abs_PKGDIR = os.path.join(os.path.abspath(PKGDIR).lower(), "")
            if abs_path_value.startswith(abs_PKGDIR):
                continue

        api.set_config_value(section, option, value, category)

    return jsonify(True)

    def _init_config(self, userdir, tempdir, storagedir, debug):
        from .config.parser import ConfigParser

        self.userdir = os.path.realpath(userdir)
        self.tempdir = os.path.realpath(tempdir)
        os.makedirs(self.userdir, exist_ok=True)
        os.makedirs(self.tempdir, exist_ok=True)

        self.config = ConfigParser(self.userdir)

        if debug is None:
            if self.config.get("general", "debug_mode"):
                debug_level = self.config.get("general", "debug_level")
                self._debug = self.DEBUG_LEVEL_MAP[debug_level]
        else:
            self._debug = max(0, int(debug))

        # If no argument set, read storage dir from config file,
        # otherwise save setting to config dir
        if storagedir is None:
            storagedir = self.config.get("general", "storage_folder")

        else:
            self.config.set("general", "storage_folder", storagedir)

        # Make sure storage_folder is not empty
        # and also not inside dangerous locations
        correct_case = lambda x: x.lower() if os.name == "nt" else x
        directories = [
            correct_case(os.path.join(os.path.realpath(d), "") )
            for d in [storagedir or PKGDIR, PKGDIR, userdir]
        ]
        is_bad_dir = any(directories[0].startswith(d) for d in directories[1:])

        if not storagedir or is_bad_dir:
            self.config.set("general", "storage_folder", "~/Downloads/pyLoad")
            storagedir = self.config.get("general", "storage_folder")

        os.makedirs(storagedir, exist_ok=True)

        if not self._dry_run:
            self.config.save()  #: save so config files gets filled

    def set_config_value(self, category, option, value, section="core"):
        """
        Set new config value.

        :param category:
        :param option:
        :param value: new config value
        :param section: 'plugin' or 'core
        """
        self.pyload.addon_manager.dispatch_event(
            "config_changed", category, option, value, section
        )

        if section == "core":
            if category == "general" and option == "storage_folder":
                # Forbid setting the download folder inside dangerous locations
                correct_case = lambda x: x.lower() if os.name == "nt" else x
                directories = [
                    correct_case(os.path.join(os.path.realpath(d), ""))
                    for d in [value, PKGDIR, self.pyload.userdir]
                ]
                if any(directories[0].startswith(d) for d in directories[1:]):
                    return

            self.pyload.config.set(category, option, value)

            if category == "download" and option in (
                "limit_speed",
                "max_speed",
            ):  #: not such a nice method to update the limit
                self.pyload.request_factory.update_bucket()

        elif section == "plugin":
            self.pyload.config.set_plugin(category, option, value)

def save_config():
    api = flask.current_app.config["PYLOAD_API"]
    category = flask.request.args.get('category')
    if category not in ("core", "plugin"):
        return jsonify(False), 500

    for key, value in flask.request.form.items():
        try:
            section, option = key.split("|")
        except ValueError:
            continue

        api.set_config_value(section, option, value, category)

    return jsonify(True)

--- a/src/pyload/core/__init__.py
+++ b/src/pyload/core/__init__.py
@@ -19,12 +19,23 @@
         # otherwise save setting to config dir
         if storagedir is None:
             storagedir = self.config.get("general", "storage_folder")
-            # Make sure storage_folder is not empty
-            if not storagedir:
-                self.config.set("general", "storage_folder", "~/Downloads/pyLoad")
-                storagedir = self.config.get("general", "storage_folder")
+
         else:
             self.config.set("general", "storage_folder", storagedir)
+
+        # Make sure storage_folder is not empty
+        # and also not inside dangerous locations
+        correct_case = lambda x: x.lower() if os.name == "nt" else x
+        directories = [
+            correct_case(os.path.join(os.path.realpath(d), "") )
+            for d in [storagedir or PKGDIR, PKGDIR, userdir]
+        ]
+        is_bad_dir = any(directories[0].startswith(d) for d in directories[1:])
+
+        if not storagedir or is_bad_dir:
+            self.config.set("general", "storage_folder", "~/Downloads/pyLoad")
+            storagedir = self.config.get("general", "storage_folder")
+
         os.makedirs(storagedir, exist_ok=True)

         if not self._dry_run:

--- a/src/pyload/core/api/__init__.py
+++ b/src/pyload/core/api/__init__.py
@@ -12,12 +12,22 @@
         )

         if section == "core":
-            self.pyload.config[category][option] = value
+            if category == "general" and option == "storage_folder":
+                # Forbid setting the download folder inside dangerous locations
+                correct_case = lambda x: x.lower() if os.name == "nt" else x
+                directories = [
+                    correct_case(os.path.join(os.path.realpath(d), ""))
+                    for d in [value, PKGDIR, self.pyload.userdir]
+                ]
+                if any(directories[0].startswith(d) for d in directories[1:]):
+                    return

-            if option in (
+            self.pyload.config.set(category, option, value)
+
+            if category == "download" and option in (
                 "limit_speed",
                 "max_speed",
-            ):  #: not so nice to update the limit
+            ):  #: not such a nice method to update the limit
                 self.pyload.request_factory.update_bucket()

         elif section == "plugin":

--- a/src/pyload/webui/app/blueprints/json_blueprint.py
+++ b/src/pyload/webui/app/blueprints/json_blueprint.py
@@ -7,14 +7,8 @@
     for key, value in flask.request.form.items():
         try:
             section, option = key.split("|")
-        except Exception:
+        except ValueError:
             continue
-
-        if section == 'general' and option=='storage_folder':
-            abs_path_value = os.path.join(os.path.abspath(value).lower(), "")
-            abs_PKGDIR = os.path.join(os.path.abspath(PKGDIR).lower(), "")
-            if abs_path_value.startswith(abs_PKGDIR):
-                continue

         api.set_config_value(section, option, value, category)


lifion-verify-dependencies through 1.1.0 is vulnerable to OS command injection via a crafted dependency name on the scanned project's package.json file.

async function getLatestVersions(name) {
  const { stdout } = await execAsync(`npm view ${name} versions --json`);
  try {
    return JSON.parse(stdout);
  } catch (err) {
    throw new Error(`Failed to parse output from NPM view - ${err.toString()}`);
  }
}

async function getLatestTag(name) {
  try {
    const { stdout } = await execAsync(`npm view ${name} dist-tags --json`);
    const { latest } = JSON.parse(stdout);
    return latest;
  } catch (err) {
    throw new Error(`Failed to parse output from NPM view - ${err.toString()}`);
  }
}

async function getLatestVersions(name) {
  isValidNpmPackageName(name);
  const { stdout } = await execAsync(`npm view ${name} versions --json`);
  try {
    return JSON.parse(stdout);
  } catch (err) {
    throw new Error(`Failed to parse output from NPM view - ${err.toString()}`);
  }
}

async function getLatestTag(name) {
  isValidNpmPackageName(name);
  try {
    const { stdout } = await execAsync(`npm view ${name} dist-tags --json`);
    const { latest } = JSON.parse(stdout);
    return latest;
  } catch (err) {
    throw new Error(`Failed to parse output from NPM view - ${err.toString()}`);
  }
}

function isValidNpmPackageName(name) {
  const { errors } = validatePackageName(name);
  if (errors) {
    throw new Error(`NPM package name: "${name}" is invalid. ${errors}`);
  }
}

--- a/lib/index.js
+++ b/lib/index.js
@@ -1,4 +1,5 @@
 async function getLatestVersions(name) {
+  isValidNpmPackageName(name);
   const { stdout } = await execAsync(`npm view ${name} versions --json`);
   try {
     return JSON.parse(stdout);

--- a/lib/index.js
+++ b/lib/index.js
@@ -1,4 +1,5 @@
 async function getLatestTag(name) {
+  isValidNpmPackageName(name);
   try {
     const { stdout } = await execAsync(`npm view ${name} dist-tags --json`);
     const { latest } = JSON.parse(stdout);

--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,6 @@
+function isValidNpmPackageName(name) {
+  const { errors } = validatePackageName(name);
+  if (errors) {
+    throw new Error(`NPM package name: "${name}" is invalid. ${errors}`);
+  }
+}

--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1 @@
+const validatePackageName = require('validate-npm-package-name');