Unsanitized user-input in Calibre <= 7.15.0 allow users with permissions to perform full-text searches to achieve SQL injection on the SQLite database.

    def search_annotations(self,
        fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type,
        restrict_to_book_ids, restrict_to_user, ignore_removed=False
    ):
        fts_engine_query = unicode_normalize(fts_engine_query)
        fts_table = 'annotations_fts_stemmed' if use_stemming else 'annotations_fts'
        text = 'annotations.searchable_text'
        if highlight_start is not None and highlight_end is not None:
            if snippet_size is not None:
                text = "snippet({fts_table}, 0, '{highlight_start}', '{highlight_end}', '\u2026', {snippet_size})".format(
                        fts_table=fts_table, highlight_start=highlight_start, highlight_end=highlight_end,
                        snippet_size=max(1, min(snippet_size, 64)))
            else:
                text = f"highlight({fts_table}, 0, '{highlight_start}', '{highlight_end}')"
        query = 'SELECT {0}.id, {0}.book, {0}.format, {0}.user_type, {0}.user, {0}.annot_data, {1} FROM {0} '
        query = query.format('annotations', text)
        query += ' JOIN {fts_table} ON annotations.id = {fts_table}.rowid'.format(fts_table=fts_table)
        query += f' WHERE {fts_table} MATCH ?'
        data = [fts_engine_query]
        if restrict_to_user:
            query += ' AND annotations.user_type = ? AND annotations.user = ?'
            data += list(restrict_to_user)
        if annotation_type:
            query += ' AND annotations.annot_type = ? '
            data.append(annotation_type)
        query += f' ORDER BY {fts_table}.rank '
        ls = json.loads
        try:
            for (rowid, book_id, fmt, user_type, user, annot_data, text) in self.execute(query, tuple(data)):
                if restrict_to_book_ids is not None and book_id not in restrict_to_book_ids:
                    continue
                try:
                    parsed_annot = ls(annot_data)
                except Exception:
                    continue
                if ignore_removed and parsed_annot.get('removed'):
                    continue
                yield {
                    'id': rowid,
                    'book_id': book_id,
                    'format': fmt,
                    'user_type': user_type,
                    'user': user,
                    'text': text,
                    'annotation': parsed_annot,
                }
        except apsw.SQLError as e:
            raise FTSQueryError(fts_engine_query, query, e)

    def search(self,
        fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, restrict_to_book_ids,
        return_text=True, process_each_result=None
    ):
        if restrict_to_book_ids is not None and not restrict_to_book_ids:
            return
        fts_engine_query = unicode_normalize(fts_engine_query)
        fts_table = 'books_fts' + ('_stemmed' if use_stemming else '')
        if return_text:
            text = 'books_text.searchable_text'
            if highlight_start is not None and highlight_end is not None:
                if snippet_size is not None:
                    text = f'''snippet("{fts_table}", 0, '{highlight_start}', '{highlight_end}', '\u2026', {max(1, min(snippet_size, 64))})'''
                else:
                    text = f'''highlight("{fts_table}", 0, '{highlight_start}', '{highlight_end}')'''
            text = ', ' + text
        else:
            text = ''
        query = 'SELECT {0}.id, {0}.book, {0}.format {1} FROM {0} '.format('books_text', text)
        query += f' JOIN {fts_table} ON fts_db.books_text.id = {fts_table}.rowid'
        query += ' WHERE '
        data = []
        conn = self.get_connection()
        temp_table_name = ''
        if restrict_to_book_ids:
            temp_table_name = f'fts_restrict_search_{next(self.temp_table_counter)}'
            conn.execute(f'CREATE TABLE temp.{temp_table_name}(x INTEGER)')
            conn.executemany(f'INSERT INTO temp.{temp_table_name} VALUES (?)', tuple((x,) for x in restrict_to_book_ids))
            query += f' fts_db.books_text.book IN temp.{temp_table_name} AND '
        query += f' "{fts_table}" MATCH ?'
        data.append(fts_engine_query)
        query += f' ORDER BY {fts_table}.rank '
        if temp_table_name:
            query += f'; DROP TABLE temp.{temp_table_name}'
        try:
            for record in conn.execute(query, tuple(data)):
                result = {
                    'id': record[0],
                    'book_id': record[1],
                    'format': record[2],
                    'text': record[3] if return_text else '',
                }
                if process_each_result is not None:
                    result = process_each_result(result)
                ret = yield result
                if ret is True:
                    break
        except apsw.SQLError as e:
            raise FTSQueryError(fts_engine_query, query, e) from e

    def search(self,
        conn, fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, restrict_to_fields=(),
        return_text=True, process_each_result=None, limit=None
    ):
        if not fts_engine_query:
            yield from self.all_notes(
                conn, restrict_to_fields, limit=limit, snippet_size=snippet_size, return_text=return_text, process_each_result=process_each_result)
            return
        fts_engine_query = unicode_normalize(fts_engine_query)
        fts_table = 'notes_fts' + ('_stemmed' if use_stemming else '')
        if return_text:
            text = 'notes.searchable_text'
            if highlight_start is not None and highlight_end is not None:
                if snippet_size is not None:
                    text = f'''snippet("{fts_table}", 0, '{highlight_start}', '{highlight_end}', '\u2026', {max(1, min(snippet_size, 64))})'''
                else:
                    text = f'''highlight("{fts_table}", 0, '{highlight_start}', '{highlight_end}')'''
            text = ', ' + text
        else:
            text = ''
        query = 'SELECT {0}.id, {0}.colname, {0}.item {1} FROM {0} '.format('notes', text)
        query += f' JOIN {fts_table} ON notes_db.notes.id = {fts_table}.rowid'
        query += ' WHERE '
        if restrict_to_fields:
            query += ' notes_db.notes.colname IN ({}) AND '.format(','.join(repeat('?', len(restrict_to_fields))))
        query += f' "{fts_table}" MATCH ?'
        query += f' ORDER BY {fts_table}.rank '
        if limit is not None:
            query += f' LIMIT {limit}'
        try:
            for record in conn.execute(query, restrict_to_fields+(fts_engine_query,)):
                result = {
                    'id': record[0],
                    'field': record[1],
                    'item_id': record[2],
                    'text': record[3] if return_text else '',
                }
                if process_each_result is not None:
                    result = process_each_result(result)
                ret = yield result
                if ret is True:
                    break
        except apsw.SQLError as e:
            raise FTSQueryError(fts_engine_query, query, e) from e

    def search_annotations(self,
        fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type,
        restrict_to_book_ids, restrict_to_user, ignore_removed=False
    ):
        fts_engine_query = unicode_normalize(fts_engine_query)
        fts_table = 'annotations_fts_stemmed' if use_stemming else 'annotations_fts'
        text = 'annotations.searchable_text'
        data = []
        if highlight_start is not None and highlight_end is not None:
            if snippet_size is not None:
                text = "snippet({fts_table}, 0, ?, ?, '\u2026', {snippet_size})".format(
                        fts_table=fts_table, snippet_size=max(1, min(snippet_size, 64)))
            else:
                text = f"highlight({fts_table}, 0, ?, ?)"
            data.append(highlight_start)
            data.append(highlight_end)
        query = 'SELECT {0}.id, {0}.book, {0}.format, {0}.user_type, {0}.user, {0}.annot_data, {1} FROM {0} '
        query = query.format('annotations', text)
        query += ' JOIN {fts_table} ON annotations.id = {fts_table}.rowid'.format(fts_table=fts_table)
        query += f' WHERE {fts_table} MATCH ?'
        data.append(fts_engine_query)
        if restrict_to_user:
            query += ' AND annotations.user_type = ? AND annotations.user = ?'
            data += list(restrict_to_user)
        if annotation_type:
            query += ' AND annotations.annot_type = ? '
            data.append(annotation_type)
        query += f' ORDER BY {fts_table}.rank '
        ls = json.loads
        try:
            for (rowid, book_id, fmt, user_type, user, annot_data, text) in self.execute(query, tuple(data)):
                if restrict_to_book_ids is not None and book_id not in restrict_to_book_ids:
                    continue
                try:
                    parsed_annot = ls(annot_data)
                except Exception:
                    continue
                if ignore_removed and parsed_annot.get('removed'):
                    continue
                yield {
                    'id': rowid,
                    'book_id': book_id,
                    'format': fmt,
                    'user_type': user_type,
                    'user': user,
                    'text': text,
                    'annotation': parsed_annot,
                }
        except apsw.SQLError as e:
            raise FTSQueryError(fts_engine_query, query, e)

    def search(self,
        fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, restrict_to_book_ids,
        return_text=True, process_each_result=None
    ):
        if restrict_to_book_ids is not None and not restrict_to_book_ids:
            return
        fts_engine_query = unicode_normalize(fts_engine_query)
        fts_table = 'books_fts' + ('_stemmed' if use_stemming else '')
        data = []
        if return_text:
            text = 'books_text.searchable_text'
            if highlight_start is not None and highlight_end is not None:
                if snippet_size is not None:
                    text = f'''snippet("{fts_table}", 0, ?, ?, '\u2026', {max(1, min(snippet_size, 64))})'''
                else:
                    text = f'''highlight("{fts_table}", 0, ?, ?)'''
                data.append(highlight_start)
                data.append(highlight_end)
            text = ', ' + text
        else:
            text = ''
        query = 'SELECT {0}.id, {0}.book, {0}.format {1} FROM {0} '.format('books_text', text)
        query += f' JOIN {fts_table} ON fts_db.books_text.id = {fts_table}.rowid'
        query += ' WHERE '
        conn = self.get_connection()
        temp_table_name = ''
        if restrict_to_book_ids:
            temp_table_name = f'fts_restrict_search_{next(self.temp_table_counter)}'
            conn.execute(f'CREATE TABLE temp.{temp_table_name}(x INTEGER)')
            conn.executemany(f'INSERT INTO temp.{temp_table_name} VALUES (?)', tuple((x,) for x in restrict_to_book_ids))
            query += f' fts_db.books_text.book IN temp.{temp_table_name} AND '
        query += f' "{fts_table}" MATCH ?'
        data.append(fts_engine_query)
        query += f' ORDER BY {fts_table}.rank '
        if temp_table_name:
            query += f'; DROP TABLE temp.{temp_table_name}'
        try:
            for record in conn.execute(query, tuple(data)):
                result = {
                    'id': record[0],
                    'book_id': record[1],
                    'format': record[2],
                    'text': record[3] if return_text else '',
                }
                if process_each_result is not None:
                    result = process_each_result(result)
                ret = yield result
                if ret is True:
                    break
        except apsw.SQLError as e:
            raise FTSQueryError(fts_engine_query, query, e) from e

    def search(self,
        conn, fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, restrict_to_fields=(),
        return_text=True, process_each_result=None, limit=None
    ):
        if not fts_engine_query:
            yield from self.all_notes(
                conn, restrict_to_fields, limit=limit, snippet_size=snippet_size, return_text=return_text, process_each_result=process_each_result)
            return
        fts_engine_query = unicode_normalize(fts_engine_query)
        fts_table = 'notes_fts' + ('_stemmed' if use_stemming else '')
        hl_data = ()
        if return_text:
            text = 'notes.searchable_text'
            if highlight_start is not None and highlight_end is not None:
                if snippet_size is not None:
                    text = f'''snippet("{fts_table}", 0, ?, ?, '\u2026', {max(1, min(snippet_size, 64))})'''
                else:
                    text = f'''highlight("{fts_table}", 0, ?, ?)'''
                hl_data = (highlight_start, highlight_end)
            text = ', ' + text
        else:
            text = ''
        query = 'SELECT {0}.id, {0}.colname, {0}.item {1} FROM {0} '.format('notes', text)
        query += f' JOIN {fts_table} ON notes_db.notes.id = {fts_table}.rowid'
        query += ' WHERE '
        if restrict_to_fields:
            query += ' notes_db.notes.colname IN ({}) AND '.format(','.join(repeat('?', len(restrict_to_fields))))
        query += f' "{fts_table}" MATCH ?'
        query += f' ORDER BY {fts_table}.rank '
        if limit is not None:
            query += f' LIMIT {limit}'
        try:
            for record in conn.execute(query, hl_data + restrict_to_fields + (fts_engine_query,)):
                result = {
                    'id': record[0],
                    'field': record[1],
                    'item_id': record[2],
                    'text': record[3] if return_text else '',
                }
                if process_each_result is not None:
                    result = process_each_result(result)
                ret = yield result
                if ret is True:
                    break
        except apsw.SQLError as e:
            raise FTSQueryError(fts_engine_query, query, e) from e

--- a/src/calibre/db/backend.py
+++ b/src/calibre/db/backend.py
@@ -5,18 +5,20 @@
         fts_engine_query = unicode_normalize(fts_engine_query)
         fts_table = 'annotations_fts_stemmed' if use_stemming else 'annotations_fts'
         text = 'annotations.searchable_text'
+        data = []
         if highlight_start is not None and highlight_end is not None:
             if snippet_size is not None:
-                text = "snippet({fts_table}, 0, '{highlight_start}', '{highlight_end}', '\u2026', {snippet_size})".format(
-                        fts_table=fts_table, highlight_start=highlight_start, highlight_end=highlight_end,
-                        snippet_size=max(1, min(snippet_size, 64)))
+                text = "snippet({fts_table}, 0, ?, ?, '\u2026', {snippet_size})".format(
+                        fts_table=fts_table, snippet_size=max(1, min(snippet_size, 64)))
             else:
-                text = f"highlight({fts_table}, 0, '{highlight_start}', '{highlight_end}')"
+                text = f"highlight({fts_table}, 0, ?, ?)"
+            data.append(highlight_start)
+            data.append(highlight_end)
         query = 'SELECT {0}.id, {0}.book, {0}.format, {0}.user_type, {0}.user, {0}.annot_data, {1} FROM {0} '
         query = query.format('annotations', text)
         query += ' JOIN {fts_table} ON annotations.id = {fts_table}.rowid'.format(fts_table=fts_table)
         query += f' WHERE {fts_table} MATCH ?'
-        data = [fts_engine_query]
+        data.append(fts_engine_query)
         if restrict_to_user:
             query += ' AND annotations.user_type = ? AND annotations.user = ?'
             data += list(restrict_to_user)

--- a/src/calibre/db/fts/connect.py
+++ b/src/calibre/db/fts/connect.py
@@ -6,20 +6,22 @@
             return
         fts_engine_query = unicode_normalize(fts_engine_query)
         fts_table = 'books_fts' + ('_stemmed' if use_stemming else '')
+        data = []
         if return_text:
             text = 'books_text.searchable_text'
             if highlight_start is not None and highlight_end is not None:
                 if snippet_size is not None:
-                    text = f'''snippet("{fts_table}", 0, '{highlight_start}', '{highlight_end}', '\u2026', {max(1, min(snippet_size, 64))})'''
+                    text = f'''snippet("{fts_table}", 0, ?, ?, '\u2026', {max(1, min(snippet_size, 64))})'''
                 else:
-                    text = f'''highlight("{fts_table}", 0, '{highlight_start}', '{highlight_end}')'''
+                    text = f'''highlight("{fts_table}", 0, ?, ?)'''
+                data.append(highlight_start)
+                data.append(highlight_end)
             text = ', ' + text
         else:
             text = ''
         query = 'SELECT {0}.id, {0}.book, {0}.format {1} FROM {0} '.format('books_text', text)
         query += f' JOIN {fts_table} ON fts_db.books_text.id = {fts_table}.rowid'
         query += ' WHERE '
-        data = []
         conn = self.get_connection()
         temp_table_name = ''
         if restrict_to_book_ids:

--- a/src/calibre/db/notes/connect.py
+++ b/src/calibre/db/notes/connect.py
@@ -8,13 +8,15 @@
             return
         fts_engine_query = unicode_normalize(fts_engine_query)
         fts_table = 'notes_fts' + ('_stemmed' if use_stemming else '')
+        hl_data = ()
         if return_text:
             text = 'notes.searchable_text'
             if highlight_start is not None and highlight_end is not None:
                 if snippet_size is not None:
-                    text = f'''snippet("{fts_table}", 0, '{highlight_start}', '{highlight_end}', '\u2026', {max(1, min(snippet_size, 64))})'''
+                    text = f'''snippet("{fts_table}", 0, ?, ?, '\u2026', {max(1, min(snippet_size, 64))})'''
                 else:
-                    text = f'''highlight("{fts_table}", 0, '{highlight_start}', '{highlight_end}')'''
+                    text = f'''highlight("{fts_table}", 0, ?, ?)'''
+                hl_data = (highlight_start, highlight_end)
             text = ', ' + text
         else:
             text = ''
@@ -28,7 +30,7 @@
         if limit is not None:
             query += f' LIMIT {limit}'
         try:
-            for record in conn.execute(query, restrict_to_fields+(fts_engine_query,)):
+            for record in conn.execute(query, hl_data + restrict_to_fields + (fts_engine_query,)):
                 result = {
                     'id': record[0],
                     'field': record[1],


Invenio-Drafts-Resources is a submission/deposit module for Invenio, a software framework for research data management. Invenio-Drafts-Resources prior to versions 0.13.7 and 0.14.6 does not properly check permissions when a record is published. The vulnerability is exploitable in a default installation of InvenioRDM. An authenticated a user is able via REST API calls to publish draft records of other users if they know the record identifier and the draft validates (e.g. all require fields filled out). An attacker is not able to modify the data in the record, and thus e.g. *cannot* change a record from restricted to public. The problem is patched in Invenio-Drafts-Resources v0.13.7 and 0.14.6, which is part of InvenioRDM v6.0.1 and InvenioRDM v7.0 respectively.

Missing Authorization

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

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

    def publish(self, id_, identity, uow=None):
        """Publish a draft.

        Idea:
            - Get the draft from the data layer (draft is not passed in)
            - Validate it more strictly than when it was originally saved
              (drafts can be incomplete but only complete drafts can be turned
              into records)
            - Create or update associated (published) record with data
        """
        self.require_permission(identity, "publish")

        # Get the draft
        draft = self.draft_cls.pid.resolve(id_, registered_only=False)

        # Validate the draft strictly - since a draft can be saved with errors
        # we do a strict validation here to make sure only valid drafts can be
        # published.
        self._validate_draft(identity, draft)

        # Create the record from the draft
        latest_id = draft.versions.latest_id
        record = self.record_cls.publish(draft)

        # Run components
        self.run_components(
            'publish', identity, draft=draft, record=record, uow=uow)

        # Commit and index
        uow.register(RecordCommitOp(record, indexer=self.indexer))
        uow.register(RecordDeleteOp(draft, force=False, indexer=self.indexer))

        if latest_id:
            self._reindex_latest(latest_id, uow=uow)

        return self.result_item(
            self, identity, record, links_tpl=self.links_item_tpl)

    def publish(self, id_, identity, uow=None):
        """Publish a draft.

        Idea:
            - Get the draft from the data layer (draft is not passed in)
            - Validate it more strictly than when it was originally saved
              (drafts can be incomplete but only complete drafts can be turned
              into records)
            - Create or update associated (published) record with data
        """
        # Get the draft
        draft = self.draft_cls.pid.resolve(id_, registered_only=False)
        self.require_permission(identity, "publish", record=draft)

        # Validate the draft strictly - since a draft can be saved with errors
        # we do a strict validation here to make sure only valid drafts can be
        # published.
        self._validate_draft(identity, draft)

        # Create the record from the draft
        latest_id = draft.versions.latest_id
        record = self.record_cls.publish(draft)

        # Run components
        self.run_components(
            'publish', identity, draft=draft, record=record, uow=uow)

        # Commit and index
        uow.register(RecordCommitOp(record, indexer=self.indexer))
        uow.register(RecordDeleteOp(draft, force=False, indexer=self.indexer))

        if latest_id:
            self._reindex_latest(latest_id, uow=uow)

        return self.result_item(
            self, identity, record, links_tpl=self.links_item_tpl)

--- a/invenio_drafts_resources/services/records/service.py
+++ b/invenio_drafts_resources/services/records/service.py
@@ -8,10 +8,9 @@
               into records)
             - Create or update associated (published) record with data
         """
-        self.require_permission(identity, "publish")
-
         # Get the draft
         draft = self.draft_cls.pid.resolve(id_, registered_only=False)
+        self.require_permission(identity, "publish", record=draft)

         # Validate the draft strictly - since a draft can be saved with errors
         # we do a strict validation here to make sure only valid drafts can be


In Kubernetes versions 1.9.0-1.9.9, 1.10.0-1.10.5, and 1.11.0-1.11.1, user input was handled insecurely while setting up volume mounts on Windows nodes, which could lead to command line argument injection.

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.

func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error {
	target = normalizeWindowsPath(target)

	if source == "tmpfs" {
		glog.V(3).Infof("azureMount: mounting source (%q), target (%q), with options (%q)", source, target, options)
		return os.MkdirAll(target, 0755)
	}

	parentDir := filepath.Dir(target)
	if err := os.MkdirAll(parentDir, 0755); err != nil {
		return err
	}

	glog.V(4).Infof("azureMount: mount options(%q) source:%q, target:%q, fstype:%q, begin to mount",
		options, source, target, fstype)
	bindSource := ""

	// tell it's going to mount azure disk or azure file according to options
	if bind, _ := isBind(options); bind {
		// mount azure disk
		bindSource = normalizeWindowsPath(source)
	} else {
		if len(options) < 2 {
			glog.Warningf("azureMount: mount options(%q) command number(%d) less than 2, source:%q, target:%q, skip mounting",
				options, len(options), source, target)
			return nil
		}

		// currently only cifs mount is supported
		if strings.ToLower(fstype) != "cifs" {
			return fmt.Errorf("azureMount: only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, options)
		}

		cmdLine := fmt.Sprintf(`$User = "%s";$PWord = ConvertTo-SecureString -String "%s" -AsPlainText -Force;`+
			`$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $PWord`,
			options[0], options[1])

		bindSource = source
		cmdLine += fmt.Sprintf(";New-SmbGlobalMapping -RemotePath %s -Credential $Credential", source)

		if output, err := exec.Command("powershell", "/c", cmdLine).CombinedOutput(); err != nil {
			return fmt.Errorf("azureMount: SmbGlobalMapping failed: %v, only SMB mount is supported now, output: %q", err, string(output))
		}
	}

	if output, err := exec.Command("cmd", "/c", "mklink", "/D", target, bindSource).CombinedOutput(); err != nil {
		glog.Errorf("mklink failed: %v, source(%q) target(%q) output: %q", err, bindSource, target, string(output))
		return err
	}

	return nil
}

func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error {
	target = normalizeWindowsPath(target)

	if source == "tmpfs" {
		glog.V(3).Infof("azureMount: mounting source (%q), target (%q), with options (%q)", source, target, options)
		return os.MkdirAll(target, 0755)
	}

	parentDir := filepath.Dir(target)
	if err := os.MkdirAll(parentDir, 0755); err != nil {
		return err
	}

	glog.V(4).Infof("azureMount: mount options(%q) source:%q, target:%q, fstype:%q, begin to mount",
		options, source, target, fstype)
	bindSource := ""

	// tell it's going to mount azure disk or azure file according to options
	if bind, _ := isBind(options); bind {
		// mount azure disk
		bindSource = normalizeWindowsPath(source)
	} else {
		if len(options) < 2 {
			glog.Warningf("azureMount: mount options(%q) command number(%d) less than 2, source:%q, target:%q, skip mounting",
				options, len(options), source, target)
			return nil
		}

		// currently only cifs mount is supported
		if strings.ToLower(fstype) != "cifs" {
			return fmt.Errorf("azureMount: only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, options)
		}

		bindSource = source

		// use PowerShell Environment Variables to store user input string to prevent command line injection
		// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1
		cmdLine := fmt.Sprintf(`$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` +
			`;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` +
			`;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential`)

		cmd := exec.Command("powershell", "/c", cmdLine)
		cmd.Env = append(os.Environ(),
			fmt.Sprintf("smbuser=%s", options[0]),
			fmt.Sprintf("smbpassword=%s", options[1]),
			fmt.Sprintf("smbremotepath=%s", source))
		if output, err := cmd.CombinedOutput(); err != nil {
			return fmt.Errorf("azureMount: SmbGlobalMapping failed: %v, only SMB mount is supported now, output: %q", err, string(output))
		}
	}

	if output, err := exec.Command("cmd", "/c", "mklink", "/D", target, bindSource).CombinedOutput(); err != nil {
		glog.Errorf("mklink failed: %v, source(%q) target(%q) output: %q", err, bindSource, target, string(output))
		return err
	}

	return nil
}

--- a/pkg/util/mount/mount_windows.go
+++ b/pkg/util/mount/mount_windows.go
@@ -31,14 +31,20 @@
 			return fmt.Errorf("azureMount: only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, options)
 		}

-		cmdLine := fmt.Sprintf(`$User = "%s";$PWord = ConvertTo-SecureString -String "%s" -AsPlainText -Force;`+
-			`$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $PWord`,
-			options[0], options[1])
+		bindSource = source

-		bindSource = source
-		cmdLine += fmt.Sprintf(";New-SmbGlobalMapping -RemotePath %s -Credential $Credential", source)
+		// use PowerShell Environment Variables to store user input string to prevent command line injection
+		// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1
+		cmdLine := fmt.Sprintf(`$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` +
+			`;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` +
+			`;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential`)

-		if output, err := exec.Command("powershell", "/c", cmdLine).CombinedOutput(); err != nil {
+		cmd := exec.Command("powershell", "/c", cmdLine)
+		cmd.Env = append(os.Environ(),
+			fmt.Sprintf("smbuser=%s", options[0]),
+			fmt.Sprintf("smbpassword=%s", options[1]),
+			fmt.Sprintf("smbremotepath=%s", source))
+		if output, err := cmd.CombinedOutput(); err != nil {
 			return fmt.Errorf("azureMount: SmbGlobalMapping failed: %v, only SMB mount is supported now, output: %q", err, string(output))
 		}
 	}


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

def _wget_download(url, fullname):
    # using wget to download url
    tmp_fullname = fullname + "_tmp"
    # \u2013user-agent
    command = f'wget -O {tmp_fullname} -t {DOWNLOAD_RETRY_LIMIT} {url}'
    subprc = subprocess.Popen(
        command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    _ = subprc.communicate()

    if subprc.returncode != 0:
        raise RuntimeError(
            f'{command} failed. Please make sure `wget` is installed or {url} exists'
        )

    shutil.move(tmp_fullname, fullname)

    return fullname

def _wget_download(url: str, fullname: str):
    try:
        assert urlparse(url).scheme in (
            'http',
            'https',
        ), 'Only support https and http url'
        # using wget to download url
        tmp_fullname = fullname + "_tmp"
        # \u2013user-agent
        command = f'wget -O {tmp_fullname} -t {DOWNLOAD_RETRY_LIMIT} {url}'
        subprc = subprocess.Popen(
            command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        _ = subprc.communicate()

        if subprc.returncode != 0:
            raise RuntimeError(
                f'{command} failed. Please make sure `wget` is installed or {url} exists'
            )

        shutil.move(tmp_fullname, fullname)

    except Exception as e:  # requests.exceptions.ConnectionError
        logger.info(f"Downloading {url} failed with exception {str(e)}")
        return False

    return fullname

--- a/python/paddle/utils/download.py
+++ b/python/paddle/utils/download.py
@@ -1,18 +1,27 @@
-def _wget_download(url, fullname):
-    # using wget to download url
-    tmp_fullname = fullname + "_tmp"
-    # \u2013user-agent
-    command = f'wget -O {tmp_fullname} -t {DOWNLOAD_RETRY_LIMIT} {url}'
-    subprc = subprocess.Popen(
-        command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
-    )
-    _ = subprc.communicate()
+def _wget_download(url: str, fullname: str):
+    try:
+        assert urlparse(url).scheme in (
+            'http',
+            'https',
+        ), 'Only support https and http url'
+        # using wget to download url
+        tmp_fullname = fullname + "_tmp"
+        # \u2013user-agent
+        command = f'wget -O {tmp_fullname} -t {DOWNLOAD_RETRY_LIMIT} {url}'
+        subprc = subprocess.Popen(
+            command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+        _ = subprc.communicate()

-    if subprc.returncode != 0:
-        raise RuntimeError(
-            f'{command} failed. Please make sure `wget` is installed or {url} exists'
-        )
+        if subprc.returncode != 0:
+            raise RuntimeError(
+                f'{command} failed. Please make sure `wget` is installed or {url} exists'
+            )

-    shutil.move(tmp_fullname, fullname)
+        shutil.move(tmp_fullname, fullname)
+
+    except Exception as e:  # requests.exceptions.ConnectionError
+        logger.info(f"Downloading {url} failed with exception {str(e)}")
+        return False

     return fullname


A directory traversal vulnerability in file upload function of Gotenberg through 6.2.1 allows an attacker to upload and overwrite any writable files outside the intended folder. This can lead to DoS, a change to program behavior, or code execution.

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.

func (ctx *Context) WithResource(directoryName string) error {
	const op string = "context.Context.WithResource"
	resolver := func() (resource.Resource, error) {
		r, err := resource.New(ctx.logger, directoryName)
		if err != nil {
			return r, err
		}
		// retrieve custom headers from request.
		for key, value := range ctx.Request().Header {
			r.WithCustomHTTPHeader(key, value[0])
		}
		// retrieve form values from request.
		for _, key := range resource.ArgKeys() {
			r.WithArg(key, ctx.FormValue(string(key)))
		}
		// write form files from request.
		form, err := ctx.MultipartForm()
		if err != nil {
			/*
				(very) special case: one and
				only one file has been sent
				and it is empty.
			*/
			if strings.Contains(err.Error(), io.EOF.Error()) {
				return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err)
			}
			return r, err
		}
		for _, files := range form.File {
			for _, fh := range files {
				in, err := fh.Open()
				if err != nil {
					return r, err
				}
				defer in.Close() // nolint: errcheck
				if err := r.WithFile(fh.Filename, in); err != nil {
					return r, err
				}
			}
		}
		return r, nil
	}
	resource, err := resolver()
	ctx.resource = resource
	if err != nil {
		return xerror.New(op, err)
	}
	return nil
}

func (ctx *Context) WithResource(directoryName string) error {
	const op string = "context.Context.WithResource"
	resolver := func() (resource.Resource, error) {
		r, err := resource.New(ctx.logger, directoryName)
		if err != nil {
			return r, err
		}
		// retrieve custom headers from request.
		for key, value := range ctx.Request().Header {
			r.WithCustomHTTPHeader(key, value[0])
		}
		// retrieve form values from request.
		for _, key := range resource.ArgKeys() {
			r.WithArg(key, ctx.FormValue(string(key)))
		}
		// write form files from request.
		form, err := ctx.MultipartForm()
		if err != nil {
			/*
				(very) special case: one and
				only one file has been sent
				and it is empty.
			*/
			if strings.Contains(err.Error(), io.EOF.Error()) {
				return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err)
			}
			return r, err
		}
		for _, files := range form.File {
			for _, fh := range files {
				in, err := fh.Open()
				if err != nil {
					return r, err
				}
				defer in.Close() // nolint: errcheck
				// avoid directory traversal.
				filename := filepath.Base(fh.Filename)
				if err := r.WithFile(filename, in); err != nil {
					return r, err
				}
			}
		}
		return r, nil
	}
	resource, err := resolver()
	ctx.resource = resource
	if err != nil {
		return xerror.New(op, err)
	}
	return nil
}

--- a/internal/app/xhttp/pkg/context/context.go
+++ b/internal/app/xhttp/pkg/context/context.go
@@ -33,7 +33,9 @@
 					return r, err
 				}
 				defer in.Close() // nolint: errcheck
-				if err := r.WithFile(fh.Filename, in); err != nil {
+				// avoid directory traversal.
+				filename := filepath.Base(fh.Filename)
+				if err := r.WithFile(filename, in); err != nil {
 					return r, err
 				}
 			}


In invoke-ai/invokeai version v5.0.2, the web API `POST /api/v1/images/delete` is vulnerable to Arbitrary File Deletion. This vulnerability allows unauthorized attackers to delete arbitrary files on the server, potentially including critical or sensitive system files such as SSH keys, SQLite databases, and configuration files. This can impact the integrity and availability of applications relying on these files.

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.

    def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
        path = self.__output_folder / image_name

        if thumbnail:
            thumbnail_name = get_thumbnail_name(image_name)
            path = self.__thumbnails_folder / thumbnail_name

        return path

    def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
        base_folder = self.__thumbnails_folder if thumbnail else self.__output_folder
        filename = get_thumbnail_name(image_name) if thumbnail else image_name

        # Strip any path information from the filename
        basename = Path(filename).name

        if basename != filename:
            raise ValueError("Invalid image name, potential directory traversal detected")

        image_path = base_folder / basename

        # Ensure the image path is within the base folder to prevent directory traversal
        resolved_base = base_folder.resolve()
        resolved_image_path = image_path.resolve()

        if not resolved_image_path.is_relative_to(resolved_base):
            raise ValueError("Image path outside outputs folder, potential directory traversal detected")

        return resolved_image_path

--- a/invokeai/app/services/image_files/image_files_disk.py
+++ b/invokeai/app/services/image_files/image_files_disk.py
@@ -1,8 +1,20 @@
     def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
-        path = self.__output_folder / image_name
+        base_folder = self.__thumbnails_folder if thumbnail else self.__output_folder
+        filename = get_thumbnail_name(image_name) if thumbnail else image_name

-        if thumbnail:
-            thumbnail_name = get_thumbnail_name(image_name)
-            path = self.__thumbnails_folder / thumbnail_name
+        # Strip any path information from the filename
+        basename = Path(filename).name

-        return path
+        if basename != filename:
+            raise ValueError("Invalid image name, potential directory traversal detected")
+
+        image_path = base_folder / basename
+
+        # Ensure the image path is within the base folder to prevent directory traversal
+        resolved_base = base_folder.resolve()
+        resolved_image_path = image_path.resolve()
+
+        if not resolved_image_path.is_relative_to(resolved_base):
+            raise ValueError("Image path outside outputs folder, potential directory traversal detected")
+
+        return resolved_image_path


The xmlhttprequest-ssl package before 1.6.1 for Node.js disables SSL certificate validation by default, because rejectUnauthorized (when the property exists but is undefined) is considered to be false within the https.request function of Node.js. In other words, no certificate is ever rejected.

Improper Certificate Validation

The product does not validate, or incorrectly validates, a certificate.

  this.send = function(data) {
    if (this.readyState != this.OPENED) {
      throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");
    }

    if (sendFlag) {
      throw new Error("INVALID_STATE_ERR: send has already been called");
    }

    var ssl = false, local = false;
    var url = Url.parse(settings.url);
    var host;
    // Determine the server
    switch (url.protocol) {
      case 'https:':
        ssl = true;
        // SSL & non-SSL both need host, no break here.
      case 'http:':
        host = url.hostname;
        break;

      case 'file:':
        local = true;
        break;

      case undefined:
      case '':
        host = "localhost";
        break;

      default:
        throw new Error("Protocol not supported.");
    }

    // Load files off the local filesystem (file://)
    if (local) {
      if (settings.method !== "GET") {
        throw new Error("XMLHttpRequest: Only GET method is supported");
      }

      if (settings.async) {
        fs.readFile(url.pathname, 'utf8', function(error, data) {
          if (error) {
            self.handleError(error);
          } else {
            self.status = 200;
            self.responseText = data;
            setState(self.DONE);
          }
        });
      } else {
        try {
          this.responseText = fs.readFileSync(url.pathname, 'utf8');
          this.status = 200;
          setState(self.DONE);
        } catch(e) {
          this.handleError(e);
        }
      }

      return;
    }

    // Default to port 80. If accessing localhost on another port be sure
    // to use http://localhost:port/path
    var port = url.port || (ssl ? 443 : 80);
    // Add query string if one is used
    var uri = url.pathname + (url.search ? url.search : '');

    // Set the Host header or the server may reject the request
    headers["Host"] = host;
    if (!((ssl && port === 443) || port === 80)) {
      headers["Host"] += ':' + url.port;
    }

    // Set Basic Auth if necessary
    if (settings.user) {
      if (typeof settings.password == "undefined") {
        settings.password = "";
      }
      var authBuf = new Buffer(settings.user + ":" + settings.password);
      headers["Authorization"] = "Basic " + authBuf.toString("base64");
    }

    // Set content length header
    if (settings.method === "GET" || settings.method === "HEAD") {
      data = null;
    } else if (data) {
      headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);

      if (!headers["Content-Type"]) {
        headers["Content-Type"] = "text/plain;charset=UTF-8";
      }
    } else if (settings.method === "POST") {
      // For a post with no data set Content-Length: 0.
      // This is required by buggy servers that don't meet the specs.
      headers["Content-Length"] = 0;
    }

    var agent = opts.agent || false;
    var options = {
      host: host,
      port: port,
      path: uri,
      method: settings.method,
      headers: headers,
      agent: agent
    };

    if (ssl) {
      options.pfx = opts.pfx;
      options.key = opts.key;
      options.passphrase = opts.passphrase;
      options.cert = opts.cert;
      options.ca = opts.ca;
      options.ciphers = opts.ciphers;
      options.rejectUnauthorized = opts.rejectUnauthorized;
    }

    // Reset error flag
    errorFlag = false;
    // Handle async requests
    if (settings.async) {
      // Use the proper protocol
      var doRequest = ssl ? https.request : http.request;

      // Request is being sent, set send flag
      sendFlag = true;

      // As per spec, this is called here for historical reasons.
      self.dispatchEvent("readystatechange");

      // Handler for the response
      var responseHandler = function(resp) {
        // Set response var to the response we got back
        // This is so it remains accessable outside this scope
        response = resp;
        // Check for redirect
        // @TODO Prevent looped redirects
        if (response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
          // Change URL to the redirect location
          settings.url = response.headers.location;
          var url = Url.parse(settings.url);
          // Set host var in case it's used later
          host = url.hostname;
          // Options for the new request
          var newOptions = {
            hostname: url.hostname,
            port: url.port,
            path: url.path,
            method: response.statusCode === 303 ? 'GET' : settings.method,
            headers: headers
          };

          if (ssl) {
            newOptions.pfx = opts.pfx;
            newOptions.key = opts.key;
            newOptions.passphrase = opts.passphrase;
            newOptions.cert = opts.cert;
            newOptions.ca = opts.ca;
            newOptions.ciphers = opts.ciphers;
            newOptions.rejectUnauthorized = opts.rejectUnauthorized;
          }

          // Issue the new request
          request = doRequest(newOptions, responseHandler).on('error', errorHandler);
          request.end();
          // @TODO Check if an XHR event needs to be fired here
          return;
        }

        if (response && response.setEncoding) {
          response.setEncoding("utf8");
        }

        setState(self.HEADERS_RECEIVED);
        self.status = response.statusCode;

        response.on('data', function(chunk) {
          // Make sure there's some data
          if (chunk) {
            self.responseText += chunk;
          }
          // Don't emit state changes if the connection has been aborted.
          if (sendFlag) {
            setState(self.LOADING);
          }
        });

        response.on('end', function() {
          if (sendFlag) {
            // The sendFlag needs to be set before setState is called.  Otherwise if we are chaining callbacks
            // there can be a timing issue (the callback is called and a new call is made before the flag is reset).
            sendFlag = false;
            // Discard the 'end' event if the connection has been aborted
            setState(self.DONE);
          }
        });

        response.on('error', function(error) {
          self.handleError(error);
        });
      }

      // Error handler for the request
      var errorHandler = function(error) {
        self.handleError(error);
      }

      // Create the request
      request = doRequest(options, responseHandler).on('error', errorHandler);

      // Node 0.4 and later won't accept empty data. Make sure it's needed.
      if (data) {
        request.write(data);
      }

      request.end();

      self.dispatchEvent("loadstart");
    } else { // Synchronous
      // Create a temporary file for communication with the other Node process
      var contentFile = ".node-xmlhttprequest-content-" + process.pid;
      var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
      fs.writeFileSync(syncFile, "", "utf8");
      // The async request the other Node process executes
      var execString = "var http = require('http'), https = require('https'), fs = require('fs');"
        + "var doRequest = http" + (ssl ? "s" : "") + ".request;"
        + "var options = " + JSON.stringify(options) + ";"
        + "var responseText = '';"
        + "var req = doRequest(options, function(response) {"
        + "response.setEncoding('utf8');"
        + "response.on('data', function(chunk) {"
        + "  responseText += chunk;"
        + "});"
        + "response.on('end', function() {"
        + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');"
        + "fs.unlinkSync('" + syncFile + "');"
        + "});"
        + "response.on('error', function(error) {"
        + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
        + "fs.unlinkSync('" + syncFile + "');"
        + "});"
        + "}).on('error', function(error) {"
        + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
        + "fs.unlinkSync('" + syncFile + "');"
        + "});"
        + (data ? "req.write('" + data.replace(/'/g, "\\'") + "');":"")
        + "req.end();";
      // Start the other Node Process, executing this string
      var syncProc = spawn(process.argv[0], ["-e", execString]);
      var statusText;
      while(fs.existsSync(syncFile)) {
        // Wait while the sync file is empty
      }
      self.responseText = fs.readFileSync(contentFile, 'utf8');
      // Kill the child process once the file has data
      syncProc.stdin.end();
      // Remove the temporary file
      fs.unlinkSync(contentFile);
      if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
        // If the file returned an error, handle it
        var errorObj = self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "");
        self.handleError(errorObj, 503);
      } else {
        // If the file returned okay, parse its data and move to the DONE state
        self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1");
        self.responseText = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1");
        setState(self.DONE);
      }
    }
  };

  this.send = function(data) {
    if (this.readyState != this.OPENED) {
      throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");
    }

    if (sendFlag) {
      throw new Error("INVALID_STATE_ERR: send has already been called");
    }

    var ssl = false, local = false;
    var url = Url.parse(settings.url);
    var host;
    // Determine the server
    switch (url.protocol) {
      case 'https:':
        ssl = true;
        // SSL & non-SSL both need host, no break here.
      case 'http:':
        host = url.hostname;
        break;

      case 'file:':
        local = true;
        break;

      case undefined:
      case '':
        host = "localhost";
        break;

      default:
        throw new Error("Protocol not supported.");
    }

    // Load files off the local filesystem (file://)
    if (local) {
      if (settings.method !== "GET") {
        throw new Error("XMLHttpRequest: Only GET method is supported");
      }

      if (settings.async) {
        fs.readFile(url.pathname, 'utf8', function(error, data) {
          if (error) {
            self.handleError(error);
          } else {
            self.status = 200;
            self.responseText = data;
            setState(self.DONE);
          }
        });
      } else {
        try {
          this.responseText = fs.readFileSync(url.pathname, 'utf8');
          this.status = 200;
          setState(self.DONE);
        } catch(e) {
          this.handleError(e);
        }
      }

      return;
    }

    // Default to port 80. If accessing localhost on another port be sure
    // to use http://localhost:port/path
    var port = url.port || (ssl ? 443 : 80);
    // Add query string if one is used
    var uri = url.pathname + (url.search ? url.search : '');

    // Set the Host header or the server may reject the request
    headers["Host"] = host;
    if (!((ssl && port === 443) || port === 80)) {
      headers["Host"] += ':' + url.port;
    }

    // Set Basic Auth if necessary
    if (settings.user) {
      if (typeof settings.password == "undefined") {
        settings.password = "";
      }
      var authBuf = new Buffer(settings.user + ":" + settings.password);
      headers["Authorization"] = "Basic " + authBuf.toString("base64");
    }

    // Set content length header
    if (settings.method === "GET" || settings.method === "HEAD") {
      data = null;
    } else if (data) {
      headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);

      if (!headers["Content-Type"]) {
        headers["Content-Type"] = "text/plain;charset=UTF-8";
      }
    } else if (settings.method === "POST") {
      // For a post with no data set Content-Length: 0.
      // This is required by buggy servers that don't meet the specs.
      headers["Content-Length"] = 0;
    }

    var agent = opts.agent || false;
    var options = {
      host: host,
      port: port,
      path: uri,
      method: settings.method,
      headers: headers,
      agent: agent
    };

    if (ssl) {
      options.pfx = opts.pfx;
      options.key = opts.key;
      options.passphrase = opts.passphrase;
      options.cert = opts.cert;
      options.ca = opts.ca;
      options.ciphers = opts.ciphers;
      options.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true;
    }

    // Reset error flag
    errorFlag = false;
    // Handle async requests
    if (settings.async) {
      // Use the proper protocol
      var doRequest = ssl ? https.request : http.request;

      // Request is being sent, set send flag
      sendFlag = true;

      // As per spec, this is called here for historical reasons.
      self.dispatchEvent("readystatechange");

      // Handler for the response
      var responseHandler = function(resp) {
        // Set response var to the response we got back
        // This is so it remains accessable outside this scope
        response = resp;
        // Check for redirect
        // @TODO Prevent looped redirects
        if (response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
          // Change URL to the redirect location
          settings.url = response.headers.location;
          var url = Url.parse(settings.url);
          // Set host var in case it's used later
          host = url.hostname;
          // Options for the new request
          var newOptions = {
            hostname: url.hostname,
            port: url.port,
            path: url.path,
            method: response.statusCode === 303 ? 'GET' : settings.method,
            headers: headers
          };

          if (ssl) {
            newOptions.pfx = opts.pfx;
            newOptions.key = opts.key;
            newOptions.passphrase = opts.passphrase;
            newOptions.cert = opts.cert;
            newOptions.ca = opts.ca;
            newOptions.ciphers = opts.ciphers;
            newOptions.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true;
          }

          // Issue the new request
          request = doRequest(newOptions, responseHandler).on('error', errorHandler);
          request.end();
          // @TODO Check if an XHR event needs to be fired here
          return;
        }

        if (response && response.setEncoding) {
          response.setEncoding("utf8");
        }

        setState(self.HEADERS_RECEIVED);
        self.status = response.statusCode;

        response.on('data', function(chunk) {
          // Make sure there's some data
          if (chunk) {
            self.responseText += chunk;
          }
          // Don't emit state changes if the connection has been aborted.
          if (sendFlag) {
            setState(self.LOADING);
          }
        });

        response.on('end', function() {
          if (sendFlag) {
            // The sendFlag needs to be set before setState is called.  Otherwise if we are chaining callbacks
            // there can be a timing issue (the callback is called and a new call is made before the flag is reset).
            sendFlag = false;
            // Discard the 'end' event if the connection has been aborted
            setState(self.DONE);
          }
        });

        response.on('error', function(error) {
          self.handleError(error);
        });
      }

      // Error handler for the request
      var errorHandler = function(error) {
        self.handleError(error);
      }

      // Create the request
      request = doRequest(options, responseHandler).on('error', errorHandler);

      // Node 0.4 and later won't accept empty data. Make sure it's needed.
      if (data) {
        request.write(data);
      }

      request.end();

      self.dispatchEvent("loadstart");
    } else { // Synchronous
      // Create a temporary file for communication with the other Node process
      var contentFile = ".node-xmlhttprequest-content-" + process.pid;
      var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
      fs.writeFileSync(syncFile, "", "utf8");
      // The async request the other Node process executes
      var execString = "var http = require('http'), https = require('https'), fs = require('fs');"
        + "var doRequest = http" + (ssl ? "s" : "") + ".request;"
        + "var options = " + JSON.stringify(options) + ";"
        + "var responseText = '';"
        + "var req = doRequest(options, function(response) {"
        + "response.setEncoding('utf8');"
        + "response.on('data', function(chunk) {"
        + "  responseText += chunk;"
        + "});"
        + "response.on('end', function() {"
        + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');"
        + "fs.unlinkSync('" + syncFile + "');"
        + "});"
        + "response.on('error', function(error) {"
        + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
        + "fs.unlinkSync('" + syncFile + "');"
        + "});"
        + "}).on('error', function(error) {"
        + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
        + "fs.unlinkSync('" + syncFile + "');"
        + "});"
        + (data ? "req.write('" + data.replace(/'/g, "\\'") + "');":"")
        + "req.end();";
      // Start the other Node Process, executing this string
      var syncProc = spawn(process.argv[0], ["-e", execString]);
      var statusText;
      while(fs.existsSync(syncFile)) {
        // Wait while the sync file is empty
      }
      self.responseText = fs.readFileSync(contentFile, 'utf8');
      // Kill the child process once the file has data
      syncProc.stdin.end();
      // Remove the temporary file
      fs.unlinkSync(contentFile);
      if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
        // If the file returned an error, handle it
        var errorObj = self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "");
        self.handleError(errorObj, 503);
      } else {
        // If the file returned okay, parse its data and move to the DONE state
        self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1");
        self.responseText = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1");
        setState(self.DONE);
      }
    }
  };

--- a/lib/XMLHttpRequest.js
+++ b/lib/XMLHttpRequest.js
@@ -114,7 +114,7 @@
       options.cert = opts.cert;
       options.ca = opts.ca;
       options.ciphers = opts.ciphers;
-      options.rejectUnauthorized = opts.rejectUnauthorized;
+      options.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true;
     }

     // Reset error flag
@@ -159,7 +159,7 @@
             newOptions.cert = opts.cert;
             newOptions.ca = opts.ca;
             newOptions.ciphers = opts.ciphers;
-            newOptions.rejectUnauthorized = opts.rejectUnauthorized;
+            newOptions.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true;
           }

           // Issue the new request


libcontainer/user/user.go in runC before 0.1.0, as used in Docker before 1.11.2, improperly treats a numeric UID as a potential username, which allows local users to gain privileges via a numeric username in the password file in a container.

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.

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 GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
	var (
		userArg, groupArg string
		name              string
	)

	if defaults == nil {
		defaults = new(ExecUser)
	}

	// Copy over defaults.
	user := &ExecUser{
		Uid:   defaults.Uid,
		Gid:   defaults.Gid,
		Sgids: defaults.Sgids,
		Home:  defaults.Home,
	}

	// Sgids slice *cannot* be nil.
	if user.Sgids == nil {
		user.Sgids = []int{}
	}

	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
	parseLine(userSpec, &userArg, &groupArg)

	users, err := ParsePasswdFilter(passwd, func(u User) bool {
		if userArg == "" {
			return u.Uid == user.Uid
		}
		return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
	})
	if err != nil && passwd != nil {
		if userArg == "" {
			userArg = strconv.Itoa(user.Uid)
		}
		return nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
	}

	haveUser := users != nil && len(users) > 0
	if haveUser {
		// if we found any user entries that matched our filter, let's take the first one as "correct"
		name = users[0].Name
		user.Uid = users[0].Uid
		user.Gid = users[0].Gid
		user.Home = users[0].Home
	} else if userArg != "" {
		// we asked for a user but didn't find them...  let's check to see if we wanted a numeric user
		user.Uid, err = strconv.Atoi(userArg)
		if err != nil {
			// not numeric - we have to bail
			return nil, fmt.Errorf("Unable to find user %v", userArg)
		}

		// Must be inside valid uid range.
		if user.Uid < minId || user.Uid > maxId {
			return nil, ErrRange
		}

		// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
	}

	if groupArg != "" || name != "" {
		groups, err := ParseGroupFilter(group, func(g Group) bool {
			// Explicit group format takes precedence.
			if groupArg != "" {
				return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
			}

			// Check if user is a member.
			for _, u := range g.List {
				if u == name {
					return true
				}
			}

			return false
		})
		if err != nil && group != nil {
			return nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
		}

		haveGroup := groups != nil && len(groups) > 0
		if groupArg != "" {
			if haveGroup {
				// if we found any group entries that matched our filter, let's take the first one as "correct"
				user.Gid = groups[0].Gid
			} else {
				// we asked for a group but didn't find id...  let's check to see if we wanted a numeric group
				user.Gid, err = strconv.Atoi(groupArg)
				if err != nil {
					// not numeric - we have to bail
					return nil, fmt.Errorf("Unable to find group %v", groupArg)
				}

				// Ensure gid is inside gid range.
				if user.Gid < minId || user.Gid > maxId {
					return nil, ErrRange
				}

				// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
			}
		} else if haveGroup {
			// If implicit group format, fill supplementary gids.
			user.Sgids = make([]int, len(groups))
			for i, group := range groups {
				user.Sgids[i] = group.Gid
			}
		}
	}

	return user, nil
}

func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
	var (
		userArg, groupArg string
	)

	if defaults == nil {
		defaults = new(ExecUser)
	}

	// Copy over defaults.
	user := &ExecUser{
		Uid:   defaults.Uid,
		Gid:   defaults.Gid,
		Sgids: defaults.Sgids,
		Home:  defaults.Home,
	}

	// Sgids slice *cannot* be nil.
	if user.Sgids == nil {
		user.Sgids = []int{}
	}

	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
	parseLine(userSpec, &userArg, &groupArg)

	// Convert userArg and groupArg to be numeric, so we don't have to execute
	// Atoi *twice* for each iteration over lines.
	uidArg, uidErr := strconv.Atoi(userArg)
	gidArg, gidErr := strconv.Atoi(groupArg)

	users, err := ParsePasswdFilter(passwd, func(u User) bool {
		if userArg == "" {
			return u.Uid == user.Uid
		}

		if uidErr == nil {
			// If the userArg is numeric, always treat it as a UID.
			return uidArg == u.Uid
		}

		return u.Name == userArg
	})
	if err != nil && passwd != nil {
		if userArg == "" {
			userArg = strconv.Itoa(user.Uid)
		}
		return nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
	}

	var matchedUserName string
	if len(users) > 0 {
		// First match wins, even if there's more than one matching entry.
		matchedUserName = users[0].Name
		user.Uid = users[0].Uid
		user.Gid = users[0].Gid
		user.Home = users[0].Home
	} else if userArg != "" {
		// If we can't find a user with the given username, the only other valid
		// option is if it's a numeric username with no associated entry in passwd.

		if uidErr != nil {
			// Not numeric.
			return nil, fmt.Errorf("unable to find user %s: %v", userArg, ErrNoPasswdEntries)
		}
		user.Uid = uidArg

		// Must be inside valid uid range.
		if user.Uid < minId || user.Uid > maxId {
			return nil, ErrRange
		}

		// Okay, so it's numeric. We can just roll with this.
	}

	// On to the groups. If we matched a username, we need to do this because of
	// the supplementary group IDs.
	if groupArg != "" || matchedUserName != "" {
		groups, err := ParseGroupFilter(group, func(g Group) bool {
			// If the group argument isn't explicit, we'll just search for it.
			if groupArg == "" {
				// Check if user is a member of this group.
				for _, u := range g.List {
					if u == matchedUserName {
						return true
					}
				}
				return false
			}

			if gidErr == nil {
				// If the groupArg is numeric, always treat it as a GID.
				return gidArg == g.Gid
			}

			return g.Name == groupArg
		})
		if err != nil && group != nil {
			return nil, fmt.Errorf("unable to find groups for spec %v: %v", matchedUserName, err)
		}

		haveGroup := groups != nil && len(groups) > 0
		if groupArg != "" {
			if haveGroup {
				// if we found any group entries that matched our filter, let's take the first one as "correct"
				user.Gid = groups[0].Gid
			} else if groupArg != "" {
				// If we can't find a group with the given name, the only other valid
				// option is if it's a numeric group name with no associated entry in group.

				if gidErr != nil {
					// Not numeric.
					return nil, fmt.Errorf("unable to find group %s: %v", groupArg, ErrNoGroupEntries)
				}
				user.Gid = gidArg

				// Ensure gid is inside gid range.
				if user.Gid < minId || user.Gid > maxId {
					return nil, ErrRange
				}

				// Okay, so it's numeric. We can just roll with this.
			}
		} else if len(groups) > 0 {
			// Supplementary group ids only make sense if in the implicit form.
			user.Sgids = make([]int, len(groups))
			for i, group := range groups {
				user.Sgids[i] = group.Gid
			}
		}
	}

	return user, nil
}

--- a/libcontainer/user/user.go
+++ b/libcontainer/user/user.go
@@ -1,7 +1,6 @@
 func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
 	var (
 		userArg, groupArg string
-		name              string
 	)

 	if defaults == nil {
@@ -24,11 +23,22 @@
 	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
 	parseLine(userSpec, &userArg, &groupArg)

+	// Convert userArg and groupArg to be numeric, so we don't have to execute
+	// Atoi *twice* for each iteration over lines.
+	uidArg, uidErr := strconv.Atoi(userArg)
+	gidArg, gidErr := strconv.Atoi(groupArg)
+
 	users, err := ParsePasswdFilter(passwd, func(u User) bool {
 		if userArg == "" {
 			return u.Uid == user.Uid
 		}
-		return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
+
+		if uidErr == nil {
+			// If the userArg is numeric, always treat it as a UID.
+			return uidArg == u.Uid
+		}
+
+		return u.Name == userArg
 	})
 	if err != nil && passwd != nil {
 		if userArg == "" {
@@ -37,47 +47,55 @@
 		return nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
 	}

-	haveUser := users != nil && len(users) > 0
-	if haveUser {
-		// if we found any user entries that matched our filter, let's take the first one as "correct"
-		name = users[0].Name
+	var matchedUserName string
+	if len(users) > 0 {
+		// First match wins, even if there's more than one matching entry.
+		matchedUserName = users[0].Name
 		user.Uid = users[0].Uid
 		user.Gid = users[0].Gid
 		user.Home = users[0].Home
 	} else if userArg != "" {
-		// we asked for a user but didn't find them...  let's check to see if we wanted a numeric user
-		user.Uid, err = strconv.Atoi(userArg)
-		if err != nil {
-			// not numeric - we have to bail
-			return nil, fmt.Errorf("Unable to find user %v", userArg)
+		// If we can't find a user with the given username, the only other valid
+		// option is if it's a numeric username with no associated entry in passwd.
+
+		if uidErr != nil {
+			// Not numeric.
+			return nil, fmt.Errorf("unable to find user %s: %v", userArg, ErrNoPasswdEntries)
 		}
+		user.Uid = uidArg

 		// Must be inside valid uid range.
 		if user.Uid < minId || user.Uid > maxId {
 			return nil, ErrRange
 		}

-		// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
+		// Okay, so it's numeric. We can just roll with this.
 	}

-	if groupArg != "" || name != "" {
+	// On to the groups. If we matched a username, we need to do this because of
+	// the supplementary group IDs.
+	if groupArg != "" || matchedUserName != "" {
 		groups, err := ParseGroupFilter(group, func(g Group) bool {
-			// Explicit group format takes precedence.
-			if groupArg != "" {
-				return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
+			// If the group argument isn't explicit, we'll just search for it.
+			if groupArg == "" {
+				// Check if user is a member of this group.
+				for _, u := range g.List {
+					if u == matchedUserName {
+						return true
+					}
+				}
+				return false
 			}

-			// Check if user is a member.
-			for _, u := range g.List {
-				if u == name {
-					return true
-				}
+			if gidErr == nil {
+				// If the groupArg is numeric, always treat it as a GID.
+				return gidArg == g.Gid
 			}

-			return false
+			return g.Name == groupArg
 		})
 		if err != nil && group != nil {
-			return nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
+			return nil, fmt.Errorf("unable to find groups for spec %v: %v", matchedUserName, err)
 		}

 		haveGroup := groups != nil && len(groups) > 0
@@ -85,23 +103,25 @@
 			if haveGroup {
 				// if we found any group entries that matched our filter, let's take the first one as "correct"
 				user.Gid = groups[0].Gid
-			} else {
-				// we asked for a group but didn't find id...  let's check to see if we wanted a numeric group
-				user.Gid, err = strconv.Atoi(groupArg)
-				if err != nil {
-					// not numeric - we have to bail
-					return nil, fmt.Errorf("Unable to find group %v", groupArg)
+			} else if groupArg != "" {
+				// If we can't find a group with the given name, the only other valid
+				// option is if it's a numeric group name with no associated entry in group.
+
+				if gidErr != nil {
+					// Not numeric.
+					return nil, fmt.Errorf("unable to find group %s: %v", groupArg, ErrNoGroupEntries)
 				}
+				user.Gid = gidArg

 				// Ensure gid is inside gid range.
 				if user.Gid < minId || user.Gid > maxId {
 					return nil, ErrRange
 				}

-				// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
+				// Okay, so it's numeric. We can just roll with this.
 			}
-		} else if haveGroup {
-			// If implicit group format, fill supplementary gids.
+		} else if len(groups) > 0 {
+			// Supplementary group ids only make sense if in the implicit form.
 			user.Sgids = make([]int, len(groups))
 			for i, group := range groups {
 				user.Sgids[i] = group.Gid


JupyterHub is an open source multi-user server for Jupyter notebooks. In affected versions users who have multiple JupyterLab tabs open in the same browser session, may see incomplete logout from the single-user server, as fresh credentials (for the single-user server only, not the Hub) reinstated after logout, if another active JupyterLab session is open while the logout takes place. Upgrade to JupyterHub 1.5. For distributed deployments, it is jupyterhub in the _user_ environment that needs patching. There are no patches necessary in the Hub environment. The only workaround is to make sure that only one JupyterLab tab is open when you log out.

Insufficient Session Expiration

According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."

    def get_current_user(self):
        """Tornado's authentication method

        Returns:
            user_model (dict): The user model, if a user is identified, None if authentication fails.
        """
        if hasattr(self, '_hub_auth_user_cache'):
            return self._hub_auth_user_cache
        user_model = self.hub_auth.get_user(self)
        if not user_model:
            self._hub_auth_user_cache = None
            return
        try:
            self._hub_auth_user_cache = self.check_hub_user(user_model)
        except UserNotAllowed as e:
            # cache None, in case get_user is called again while processing the error
            self._hub_auth_user_cache = None
            # Override redirect so if/when tornado @web.authenticated
            # tries to redirect to login URL, 403 will be raised instead.
            # This is not the best, but avoids problems that can be caused
            # when get_current_user is allowed to raise.
            def raise_on_redirect(*args, **kwargs):
                raise HTTPError(
                    403, "{kind} {name} is not allowed.".format(**user_model)
                )

            self.redirect = raise_on_redirect
            return
        except Exception:
            self._hub_auth_user_cache = None
            raise

        # store tokens passed via url or header in a cookie for future requests
        url_token = self.hub_auth.get_token(self)
        if (
            user_model
            and url_token
            and getattr(self, '_token_authenticated', False)
            and hasattr(self.hub_auth, 'set_cookie')
        ):
            # authenticated via `?token=`
            # set a cookie for future requests
            # hub_auth.set_cookie is only available on HubOAuth
            self.hub_auth.set_cookie(self, url_token)
        return self._hub_auth_user_cache

    def get_current_user(self):
        """Tornado's authentication method

        Returns:
            user_model (dict): The user model, if a user is identified, None if authentication fails.
        """
        if hasattr(self, '_hub_auth_user_cache'):
            return self._hub_auth_user_cache
        user_model = self.hub_auth.get_user(self)
        if not user_model:
            self._hub_auth_user_cache = None
            return
        try:
            self._hub_auth_user_cache = self.check_hub_user(user_model)
        except UserNotAllowed as e:
            # cache None, in case get_user is called again while processing the error
            self._hub_auth_user_cache = None
            # Override redirect so if/when tornado @web.authenticated
            # tries to redirect to login URL, 403 will be raised instead.
            # This is not the best, but avoids problems that can be caused
            # when get_current_user is allowed to raise.
            def raise_on_redirect(*args, **kwargs):
                raise HTTPError(
                    403, "{kind} {name} is not allowed.".format(**user_model)
                )

            self.redirect = raise_on_redirect
            return
        except Exception:
            self._hub_auth_user_cache = None
            raise

        # store ?token=... tokens passed via url in a cookie for future requests
        url_token = self.get_argument('token', '')
        if (
            user_model
            and url_token
            and getattr(self, '_token_authenticated', False)
            and hasattr(self.hub_auth, 'set_cookie')
        ):
            # authenticated via `?token=`
            # set a cookie for future requests
            # hub_auth.set_cookie is only available on HubOAuth
            self.hub_auth.set_cookie(self, url_token)
        return self._hub_auth_user_cache

--- a/jupyterhub/services/auth.py
+++ b/jupyterhub/services/auth.py
@@ -30,8 +30,8 @@
             self._hub_auth_user_cache = None
             raise

-        # store tokens passed via url or header in a cookie for future requests
-        url_token = self.hub_auth.get_token(self)
+        # store ?token=... tokens passed via url in a cookie for future requests
+        url_token = self.get_argument('token', '')
         if (
             user_model
             and url_token


GuardDog is a CLI tool to identify malicious PyPI packages. Versions prior to 0.1.5 are vulnerable to Relative Path Traversal when scanning a specially-crafted local PyPI package. Running GuardDog against a specially-crafted package can allow an attacker to write an arbitrary file on the machine where GuardDog is executed due to a path traversal vulnerability when extracting the .tar.gz file of the package being scanned, which exists by design in the tarfile.TarFile.extractall function. This issue is patched in version 0.1.5.

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.

    def scan_local(self, path, rules=None) -> dict:
        """
        Scans local package

        Args:
            path (str): path to package
            rules (set, optional): Set of rule names to use. Defaults to all rules.

        Raises:
            Exception: Analyzer exception

        Returns:
            dict: Analyzer output with rules to results mapping
        """

        if rules is not None:
            rules = set(rules)

        if os.path.exists(path):
            if path.endswith('.tar.gz'):
                with tempfile.TemporaryDirectory() as tmpdirname:
                    tarfile.open(path).extractall(tmpdirname)
                    return self.analyzer.analyze_sourcecode(tmpdirname, rules=rules)
            elif os.path.isdir(path):
                return self.analyzer.analyze_sourcecode(path, rules=rules)
        else:
            raise Exception(f"Path {path} does not exist.")

    def scan_local(self, path, rules=None) -> dict:
        """
        Scans local package

        Args:
            path (str): path to package
            rules (set, optional): Set of rule names to use. Defaults to all rules.

        Raises:
            Exception: Analyzer exception

        Returns:
            dict: Analyzer output with rules to results mapping
        """

        if rules is not None:
            rules = set(rules)

        if os.path.exists(path):
            if path.endswith('.tar.gz'):
                with tempfile.TemporaryDirectory() as tmpdirname:
                    tarsafe.open(path).extractall(tmpdirname)
                    return self.analyzer.analyze_sourcecode(tmpdirname, rules=rules)
            elif os.path.isdir(path):
                return self.analyzer.analyze_sourcecode(path, rules=rules)
        else:
            raise Exception(f"Path {path} does not exist.")

--- a/guarddog/scanners/package_scanner.py
+++ b/guarddog/scanners/package_scanner.py
@@ -19,7 +19,7 @@
         if os.path.exists(path):
             if path.endswith('.tar.gz'):
                 with tempfile.TemporaryDirectory() as tmpdirname:
-                    tarfile.open(path).extractall(tmpdirname)
+                    tarsafe.open(path).extractall(tmpdirname)
                     return self.analyzer.analyze_sourcecode(tmpdirname, rules=rules)
             elif os.path.isdir(path):
                 return self.analyzer.analyze_sourcecode(path, rules=rules)


transport.py in the SSH server implementation of Paramiko before 1.17.6, 1.18.x before 1.18.5, 2.0.x before 2.0.8, 2.1.x before 2.1.5, 2.2.x before 2.2.3, 2.3.x before 2.3.2, and 2.4.x before 2.4.1 does not properly check whether authentication is completed before processing other requests, as demonstrated by channel-open. A customized SSH client can simply skip the authentication step.

Improper Authentication

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

    def run(self):
        # (use the exposed "run" method, because if we specify a thread target
        # of a private method, threading.Thread will keep a reference to it
        # indefinitely, creating a GC cycle and not letting Transport ever be
        # GC'd. it's a bug in Thread.)

        # Hold reference to 'sys' so we can test sys.modules to detect
        # interpreter shutdown.
        self.sys = sys

        # active=True occurs before the thread is launched, to avoid a race
        _active_threads.append(self)
        tid = hex(long(id(self)) & xffffffff)
        if self.server_mode:
            self._log(DEBUG, 'starting thread (server mode): %s' % tid)
        else:
            self._log(DEBUG, 'starting thread (client mode): %s' % tid)
        try:
            try:
                self.packetizer.write_all(b(self.local_version + '\r\n'))
                self._log(DEBUG, 'Local version/idstring: %s' % self.local_version) # noqa
                self._check_banner()
                # The above is actually very much part of the handshake, but
                # sometimes the banner can be read but the machine is not
                # responding, for example when the remote ssh daemon is loaded
                # in to memory but we can not read from the disk/spawn a new
                # shell.
                # Make sure we can specify a timeout for the initial handshake.
                # Re-use the banner timeout for now.
                self.packetizer.start_handshake(self.handshake_timeout)
                self._send_kex_init()
                self._expect_packet(MSG_KEXINIT)

                while self.active:
                    if self.packetizer.need_rekey() and not self.in_kex:
                        self._send_kex_init()
                    try:
                        ptype, m = self.packetizer.read_message()
                    except NeedRekeyException:
                        continue
                    if ptype == MSG_IGNORE:
                        continue
                    elif ptype == MSG_DISCONNECT:
                        self._parse_disconnect(m)
                        self.active = False
                        self.packetizer.close()
                        break
                    elif ptype == MSG_DEBUG:
                        self._parse_debug(m)
                        continue
                    if len(self._expected_packet) > 0:
                        if ptype not in self._expected_packet:
                            raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype)) # noqa
                        self._expected_packet = tuple()
                        if (ptype >= 30) and (ptype <= 41):
                            self.kex_engine.parse_next(ptype, m)
                            continue

                    if ptype in self._handler_table:
                        self._handler_table[ptype](self, m)
                    elif ptype in self._channel_handler_table:
                        chanid = m.get_int()
                        chan = self._channels.get(chanid)
                        if chan is not None:
                            self._channel_handler_table[ptype](chan, m)
                        elif chanid in self.channels_seen:
                            self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) # noqa
                        else:
                            self._log(ERROR, 'Channel request for unknown channel %d' % chanid) # noqa
                            self.active = False
                            self.packetizer.close()
                    elif (
                        self.auth_handler is not None and
                        ptype in self.auth_handler._handler_table
                    ):
                        handler = self.auth_handler._handler_table[ptype]
                        handler(self.auth_handler, m)
                        if len(self._expected_packet) > 0:
                            continue
                    else:
                        self._log(WARNING, 'Oops, unhandled type %d' % ptype)
                        msg = Message()
                        msg.add_byte(cMSG_UNIMPLEMENTED)
                        msg.add_int(m.seqno)
                        self._send_message(msg)
                    self.packetizer.complete_handshake()
            except SSHException as e:
                self._log(ERROR, 'Exception: ' + str(e))
                self._log(ERROR, util.tb_strings())
                self.saved_exception = e
            except EOFError as e:
                self._log(DEBUG, 'EOF in transport thread')
                self.saved_exception = e
            except socket.error as e:
                if type(e.args) is tuple:
                    if e.args:
                        emsg = '%s (%d)' % (e.args[1], e.args[0])
                    else:  # empty tuple, e.g. socket.timeout
                        emsg = str(e) or repr(e)
                else:
                    emsg = e.args
                self._log(ERROR, 'Socket exception: ' + emsg)
                self.saved_exception = e
            except Exception as e:
                self._log(ERROR, 'Unknown exception: ' + str(e))
                self._log(ERROR, util.tb_strings())
                self.saved_exception = e
            _active_threads.remove(self)
            for chan in list(self._channels.values()):
                chan._unlink()
            if self.active:
                self.active = False
                self.packetizer.close()
                if self.completion_event is not None:
                    self.completion_event.set()
                if self.auth_handler is not None:
                    self.auth_handler.abort()
                for event in self.channel_events.values():
                    event.set()
                try:
                    self.lock.acquire()
                    self.server_accept_cv.notify()
                finally:
                    self.lock.release()
            self.sock.close()
        except:
            # Don't raise spurious 'NoneType has no attribute X' errors when we
            # wake up during interpreter shutdown. Or rather -- raise
            # everything *if* sys.modules (used as a convenient sentinel)
            # appears to still exist.
            if self.sys.modules is not None:
                raise

    def _ensure_authed(self, ptype, message):
        """
        Checks message type against current auth state.

        If server mode, and auth has not succeeded, and the message is of a
        post-auth type (channel open or global request) an appropriate error
        response Message is crafted and returned to caller for sending.

        Otherwise (client mode, authed, or pre-auth message) returns None.
        """
        if (
            not self.server_mode
            or ptype <= HIGHEST_USERAUTH_MESSAGE_ID
            or self.is_authenticated()
        ):
            return None
        # WELP. We must be dealing with someone trying to do non-auth things
        # without being authed. Tell them off, based on message class.
        reply = Message()
        # Global requests have no details, just failure.
        if ptype == MSG_GLOBAL_REQUEST:
            reply.add_byte(cMSG_REQUEST_FAILURE)
        # Channel opens let us reject w/ a specific type + message.
        elif ptype == MSG_CHANNEL_OPEN:
            kind = message.get_text()
            chanid = message.get_int()
            reply.add_byte(cMSG_CHANNEL_OPEN_FAILURE)
            reply.add_int(chanid)
            reply.add_int(OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED)
            reply.add_string('')
            reply.add_string('en')
        # NOTE: Post-open channel messages do not need checking; the above will
        # reject attemps to open channels, meaning that even if a malicious
        # user tries to send a MSG_CHANNEL_REQUEST, it will simply fall under
        # the logic that handles unknown channel IDs (as the channel list will
        # be empty.)
        return reply

    def run(self):
        # (use the exposed "run" method, because if we specify a thread target
        # of a private method, threading.Thread will keep a reference to it
        # indefinitely, creating a GC cycle and not letting Transport ever be
        # GC'd. it's a bug in Thread.)

        # Hold reference to 'sys' so we can test sys.modules to detect
        # interpreter shutdown.
        self.sys = sys

        # active=True occurs before the thread is launched, to avoid a race
        _active_threads.append(self)
        tid = hex(long(id(self)) & xffffffff)
        if self.server_mode:
            self._log(DEBUG, 'starting thread (server mode): %s' % tid)
        else:
            self._log(DEBUG, 'starting thread (client mode): %s' % tid)
        try:
            try:
                self.packetizer.write_all(b(self.local_version + '\r\n'))
                self._log(DEBUG, 'Local version/idstring: %s' % self.local_version) # noqa
                self._check_banner()
                # The above is actually very much part of the handshake, but
                # sometimes the banner can be read but the machine is not
                # responding, for example when the remote ssh daemon is loaded
                # in to memory but we can not read from the disk/spawn a new
                # shell.
                # Make sure we can specify a timeout for the initial handshake.
                # Re-use the banner timeout for now.
                self.packetizer.start_handshake(self.handshake_timeout)
                self._send_kex_init()
                self._expect_packet(MSG_KEXINIT)

                while self.active:
                    if self.packetizer.need_rekey() and not self.in_kex:
                        self._send_kex_init()
                    try:
                        ptype, m = self.packetizer.read_message()
                    except NeedRekeyException:
                        continue
                    if ptype == MSG_IGNORE:
                        continue
                    elif ptype == MSG_DISCONNECT:
                        self._parse_disconnect(m)
                        self.active = False
                        self.packetizer.close()
                        break
                    elif ptype == MSG_DEBUG:
                        self._parse_debug(m)
                        continue
                    if len(self._expected_packet) > 0:
                        if ptype not in self._expected_packet:
                            raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype)) # noqa
                        self._expected_packet = tuple()
                        if (ptype >= 30) and (ptype <= 41):
                            self.kex_engine.parse_next(ptype, m)
                            continue

                    if ptype in self._handler_table:
                        error_msg = self._ensure_authed(ptype, m)
                        if error_msg:
                            self._send_message(error_msg)
                        else:
                            self._handler_table[ptype](self, m)
                    elif ptype in self._channel_handler_table:
                        chanid = m.get_int()
                        chan = self._channels.get(chanid)
                        if chan is not None:
                            self._channel_handler_table[ptype](chan, m)
                        elif chanid in self.channels_seen:
                            self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) # noqa
                        else:
                            self._log(ERROR, 'Channel request for unknown channel %d' % chanid) # noqa
                            self.active = False
                            self.packetizer.close()
                    elif (
                        self.auth_handler is not None and
                        ptype in self.auth_handler._handler_table
                    ):
                        handler = self.auth_handler._handler_table[ptype]
                        handler(self.auth_handler, m)
                        if len(self._expected_packet) > 0:
                            continue
                    else:
                        self._log(WARNING, 'Oops, unhandled type %d' % ptype)
                        msg = Message()
                        msg.add_byte(cMSG_UNIMPLEMENTED)
                        msg.add_int(m.seqno)
                        self._send_message(msg)
                    self.packetizer.complete_handshake()
            except SSHException as e:
                self._log(ERROR, 'Exception: ' + str(e))
                self._log(ERROR, util.tb_strings())
                self.saved_exception = e
            except EOFError as e:
                self._log(DEBUG, 'EOF in transport thread')
                self.saved_exception = e
            except socket.error as e:
                if type(e.args) is tuple:
                    if e.args:
                        emsg = '%s (%d)' % (e.args[1], e.args[0])
                    else:  # empty tuple, e.g. socket.timeout
                        emsg = str(e) or repr(e)
                else:
                    emsg = e.args
                self._log(ERROR, 'Socket exception: ' + emsg)
                self.saved_exception = e
            except Exception as e:
                self._log(ERROR, 'Unknown exception: ' + str(e))
                self._log(ERROR, util.tb_strings())
                self.saved_exception = e
            _active_threads.remove(self)
            for chan in list(self._channels.values()):
                chan._unlink()
            if self.active:
                self.active = False
                self.packetizer.close()
                if self.completion_event is not None:
                    self.completion_event.set()
                if self.auth_handler is not None:
                    self.auth_handler.abort()
                for event in self.channel_events.values():
                    event.set()
                try:
                    self.lock.acquire()
                    self.server_accept_cv.notify()
                finally:
                    self.lock.release()
            self.sock.close()
        except:
            # Don't raise spurious 'NoneType has no attribute X' errors when we
            # wake up during interpreter shutdown. Or rather -- raise
            # everything *if* sys.modules (used as a convenient sentinel)
            # appears to still exist.
            if self.sys.modules is not None:
                raise

--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -57,7 +57,11 @@
                             continue

                     if ptype in self._handler_table:
-                        self._handler_table[ptype](self, m)
+                        error_msg = self._ensure_authed(ptype, m)
+                        if error_msg:
+                            self._send_message(error_msg)
+                        else:
+                            self._handler_table[ptype](self, m)
                     elif ptype in self._channel_handler_table:
                         chanid = m.get_int()
                         chan = self._channels.get(chanid)

--- /dev/null
+++ b/paramiko/transport.py
@@ -0,0 +1,37 @@
+    def _ensure_authed(self, ptype, message):
+        """
+        Checks message type against current auth state.
+
+        If server mode, and auth has not succeeded, and the message is of a
+        post-auth type (channel open or global request) an appropriate error
+        response Message is crafted and returned to caller for sending.
+
+        Otherwise (client mode, authed, or pre-auth message) returns None.
+        """
+        if (
+            not self.server_mode
+            or ptype <= HIGHEST_USERAUTH_MESSAGE_ID
+            or self.is_authenticated()
+        ):
+            return None
+        # WELP. We must be dealing with someone trying to do non-auth things
+        # without being authed. Tell them off, based on message class.
+        reply = Message()
+        # Global requests have no details, just failure.
+        if ptype == MSG_GLOBAL_REQUEST:
+            reply.add_byte(cMSG_REQUEST_FAILURE)
+        # Channel opens let us reject w/ a specific type + message.
+        elif ptype == MSG_CHANNEL_OPEN:
+            kind = message.get_text()
+            chanid = message.get_int()
+            reply.add_byte(cMSG_CHANNEL_OPEN_FAILURE)
+            reply.add_int(chanid)
+            reply.add_int(OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED)
+            reply.add_string('')
+            reply.add_string('en')
+        # NOTE: Post-open channel messages do not need checking; the above will
+        # reject attemps to open channels, meaning that even if a malicious
+        # user tries to send a MSG_CHANNEL_REQUEST, it will simply fall under
+        # the logic that handles unknown channel IDs (as the channel list will
+        # be empty.)
+        return reply