Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-scitokens for 
openSUSE:Factory checked in at 2026-04-04 19:05:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-scitokens (Old)
 and      /work/SRC/openSUSE:Factory/.python-scitokens.new.21863 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-scitokens"

Sat Apr  4 19:05:37 2026 rev:8 rq:1344371 version:1.8.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-scitokens/python-scitokens.changes        
2024-10-10 22:11:22.105997070 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-scitokens.new.21863/python-scitokens.changes 
    2026-04-04 19:07:07.412402126 +0200
@@ -1,0 +2,13 @@
+Thu Apr  2 15:09:31 UTC 2026 - Markéta Machová <[email protected]>
+
+- CVE-2026-32714: usage of str.format() to construct SQL queries
+  can lead to SQL Injection (bsc#1261203)
+  * CVE-2026-32714.patch
+- CVE-2026-32716: incorrectly validates scope paths can allow
+  access sibling paths (bsc#1261202)
+  * CVE-2026-32716.patch
+- CVE-2026-32727: normalizing path before check can lead to path
+  traversal (bsc#1261201)
+  * CVE-2026-32727.patch
+
+-------------------------------------------------------------------

New:
----
  CVE-2026-32714.patch
  CVE-2026-32716.patch
  CVE-2026-32727.patch

----------(New B)----------
  New:  can lead to SQL Injection (bsc#1261203)
  * CVE-2026-32714.patch
- CVE-2026-32716: incorrectly validates scope paths can allow
  New:  access sibling paths (bsc#1261202)
  * CVE-2026-32716.patch
- CVE-2026-32727: normalizing path before check can lead to path
  New:  traversal (bsc#1261201)
  * CVE-2026-32727.patch
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-scitokens.spec ++++++
--- /var/tmp/diff_new_pack.vjOAIC/_old  2026-04-04 19:07:07.980425415 +0200
+++ /var/tmp/diff_new_pack.vjOAIC/_new  2026-04-04 19:07:07.980425415 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-scitokens
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -25,6 +25,12 @@
 License:        Apache-2.0
 URL:            https://scitokens.org
 Source:         
https://github.com/scitokens/scitokens/archive/refs/tags/v%{version}.tar.gz#/%{bname}-%{version}.tar.gz
+# PATCH-FIX-UPSTREAM CVE-2026-32714.patch bsc#1261203
+Patch0:         CVE-2026-32714.patch
+# PATCH-FIX-UPSTREAM CVE-2026-32716.patch bsc#1261202
+Patch1:         CVE-2026-32716.patch
+# PATCH-FIX-UPSTREAM CVE-2026-32727.patch bsc#1261201
+Patch2:         CVE-2026-32727.patch
 BuildRequires:  %{python_module PyJWT >= 2.2}
 BuildRequires:  %{python_module cryptography}
 BuildRequires:  %{python_module pip}

++++++ CVE-2026-32714.patch ++++++
>From 3dba108853f2f4a6c0f2325c03779bf083c41cf2 Mon Sep 17 00:00:00 2001
From: Derek Weitzel <[email protected]>
Date: Fri, 13 Mar 2026 10:44:27 -0500
Subject: [PATCH] Refactor KeyCache SQL queries to use parameterized statements
 for security and add regression tests for SQL injection prevention

---
 src/scitokens/utils/keycache.py |  31 ++++----
 tests/test_keycache.py          | 134 ++++++++++++++++++++++++++++++++
 2 files changed, 149 insertions(+), 16 deletions(-)

Index: scitokens-1.8.1/src/scitokens/utils/keycache.py
===================================================================
--- scitokens-1.8.1.orig/src/scitokens/utils/keycache.py
+++ scitokens-1.8.1/src/scitokens/utils/keycache.py
@@ -76,7 +76,7 @@ class KeyCache(object):
         conn = sqlite3.connect(self.cache_location)
         conn.row_factory = sqlite3.Row
         curs = conn.cursor()
-        curs.execute("DELETE FROM keycache WHERE issuer = '{}' AND key_id = 
'{}'".format(issuer, key_id))
+        curs.execute("DELETE FROM keycache WHERE issuer = ? AND key_id = ?", 
[issuer, key_id])
         KeyCache._addkeyinfo(curs, issuer, key_id, public_key, 
cache_timer=cache_timer, next_update=next_update)
         conn.commit()
         conn.close()
@@ -87,14 +87,13 @@ class KeyCache(object):
         Given an open database cursor to a key cache, insert a key.
         """
         # Add the key to the cache
-        insert_key_statement = "INSERT INTO keycache VALUES('{issuer}', 
'{expiration}', '{key_id}', \
-                               '{keydata}', '{next_update}')"
+        insert_key_statement = "INSERT INTO keycache VALUES(?, ?, ?, ?, ?)"
         keydata = {
             'pub_key': public_key.public_bytes(Encoding.PEM, 
PublicFormat.SubjectPublicKeyInfo).decode('ascii'),
         }
 
-        curs.execute(insert_key_statement.format(issuer=issuer, 
expiration=time.time()+cache_timer, key_id=key_id,
-                                                 keydata=json.dumps(keydata), 
next_update=time.time()+next_update))
+        curs.execute(insert_key_statement, [issuer, time.time()+cache_timer, 
key_id,
+                                                 json.dumps(keydata), 
time.time()+next_update])
         if curs.rowcount != 1:
             raise UnableToWriteKeyCache("Unable to insert into key cache")
 
@@ -129,8 +128,7 @@ class KeyCache(object):
         # Open the connection to the database
         conn = sqlite3.connect(self.cache_location)
         curs = conn.cursor()
-        curs.execute("DELETE FROM keycache WHERE issuer = '{}' AND key_id = 
'{}'".format(issuer,
-                     key_id))
+        curs.execute("DELETE FROM keycache WHERE issuer = ? AND key_id = ?", 
[issuer, key_id])
         conn.commit()
         conn.close()
 
@@ -145,14 +143,16 @@ class KeyCache(object):
         :returns: None if no key is found.  Else, returns the public key
         """
         # Check the sql database
-        key_query = ("SELECT * FROM keycache WHERE "
-                     "issuer = '{issuer}'")
-        if key_id != None:
-            key_query += " AND key_id = '{key_id}'"
+        if key_id is not None:
+            key_query = "SELECT * FROM keycache WHERE issuer = ? AND key_id = 
?"
+            query_params = [issuer, key_id]
+        else:
+            key_query = "SELECT * FROM keycache WHERE issuer = ?"
+            query_params = [issuer]
         conn = sqlite3.connect(self.cache_location)
         conn.row_factory = sqlite3.Row
         curs = conn.cursor()
-        curs.execute(key_query.format(issuer=issuer, key_id=key_id))
+        curs.execute(key_query, query_params)
 
         row = curs.fetchone()
         conn.commit()
Index: scitokens-1.8.1/tests/test_keycache.py
===================================================================
--- scitokens-1.8.1.orig/tests/test_keycache.py
+++ scitokens-1.8.1/tests/test_keycache.py
@@ -264,5 +264,122 @@ class TestKeyCache(unittest.TestCase):
         create_webserver.shutdown_server()
 
 
+import sqlite3
+
+class TestKeyCacheSQLInjection(unittest.TestCase):
+    """
+    Regression tests to verify that SQL injection via issuer/key_id is not 
possible.
+    """
+
+    def setUp(self):
+        self.tmp_dir = tempfile.mkdtemp()
+        self.old_xdg = os.environ.get('XDG_CACHE_HOME', None)
+        os.environ['XDG_CACHE_HOME'] = self.tmp_dir
+        self.keycache = KeyCache()
+
+        # Generate a test key pair
+        self.private_key = generate_private_key(
+            public_exponent=65537,
+            key_size=2048,
+            backend=default_backend()
+        )
+        self.public_key = self.private_key.public_key()
+
+    def tearDown(self):
+        shutil.rmtree(self.tmp_dir)
+        if self.old_xdg:
+            os.environ['XDG_CACHE_HOME'] = self.old_xdg
+
+    def _count_rows(self):
+        conn = sqlite3.connect(self.keycache.cache_location)
+        curs = conn.cursor()
+        curs.execute("SELECT COUNT(*) FROM keycache")
+        count = curs.fetchone()[0]
+        conn.close()
+        return count
+
+    def test_injection_in_issuer_does_not_delete_other_rows(self):
+        """
+        With the old .format() pattern, an issuer like "x' OR '1'='1" in a
+        DELETE would wipe every row. Parameterized queries treat it as a
+        literal value, so no rows other than the exact match are affected.
+        """
+        # Insert a legitimate row
+        self.keycache.addkeyinfo("https://legit.example.com/";, "key1",
+                                 self.public_key, cache_timer=3600)
+        self.assertEqual(self._count_rows(), 1)
+
+        # Attempt injection via issuer in addkeyinfo (which DELETEs first)
+        malicious_issuer = "x' OR '1'='1"
+        self.keycache.addkeyinfo(malicious_issuer, "evil_key",
+                                 self.public_key, cache_timer=3600)
+
+        # The legitimate row must still exist, plus the new malicious-literal 
row
+        self.assertEqual(self._count_rows(), 2)
+
+    def test_injection_in_key_id_does_not_delete_other_rows(self):
+        """
+        A malicious key_id should not be able to affect other rows.
+        """
+        self.keycache.addkeyinfo("https://legit.example.com/";, "key1",
+                                 self.public_key, cache_timer=3600)
+        self.assertEqual(self._count_rows(), 1)
+
+        malicious_key_id = "x' OR '1'='1"
+        self.keycache.addkeyinfo("https://other.example.com/";, 
malicious_key_id,
+                                 self.public_key, cache_timer=3600)
+
+        self.assertEqual(self._count_rows(), 2)
+
+    def test_delete_cache_entry_with_injection_string(self):
+        """
+        _delete_cache_entry with a crafted issuer must not delete unrelated 
rows.
+        """
+        self.keycache.addkeyinfo("https://legit.example.com/";, "key1",
+                                 self.public_key, cache_timer=3600)
+        self.assertEqual(self._count_rows(), 1)
+
+        # Try to delete with an injection string — should match nothing
+        self.keycache._delete_cache_entry("x' OR '1'='1", "key1")
+        self.assertEqual(self._count_rows(), 1)
+
+    def test_union_select_injection_is_literal(self):
+        """
+        A UNION SELECT payload in the issuer should be stored as a literal
+        value, not interpreted as SQL.
+        """
+        malicious_issuer = "x' UNION SELECT * FROM keycache --"
+        self.keycache.addkeyinfo(malicious_issuer, "key1",
+                                 self.public_key, cache_timer=3600)
+        self.assertEqual(self._count_rows(), 1)
+
+        # The stored issuer should be the literal malicious string
+        conn = sqlite3.connect(self.keycache.cache_location)
+        curs = conn.cursor()
+        curs.execute("SELECT issuer FROM keycache")
+        row = curs.fetchone()
+        conn.close()
+        self.assertEqual(row[0], malicious_issuer)
+
+    def test_getkeyinfo_injection_issuer_no_leak(self):
+        """
+        getkeyinfo with an injection payload in issuer must not return
+        rows belonging to a different issuer.
+        """
+        self.keycache.addkeyinfo("https://legit.example.com/";, "key1",
+                                 self.public_key, cache_timer=3600)
+
+        # This injection string would match all rows with the old code
+        malicious_issuer = "x' OR '1'='1"
+        # getkeyinfo will not find a cached row and will try to download,
+        # which will fail — that's expected.  The important thing is it
+        # does NOT return the legit key.
+        try:
+            result = self.keycache.getkeyinfo(malicious_issuer, "key1")
+        except Exception:
+            result = None
+        self.assertIsNone(result)
+
+
 if __name__ == '__main__':
     unittest.main()

++++++ CVE-2026-32716.patch ++++++
>From 7a237c0f642efb9e8c36ac564b745895cca83583 Mon Sep 17 00:00:00 2001
From: Derek Weitzel <[email protected]>
Date: Fri, 13 Mar 2026 10:49:49 -0500
Subject: [PATCH] Add scope path matching logic and corresponding unit tests
 for Enforcer

---
 src/scitokens/scitokens.py | 15 +++++++++++---
 tests/test_scitokens.py    | 40 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 52 insertions(+), 3 deletions(-)

Index: scitokens-1.8.1/src/scitokens/scitokens.py
===================================================================
--- scitokens-1.8.1.orig/src/scitokens/scitokens.py
+++ scitokens-1.8.1/src/scitokens/scitokens.py
@@ -679,6 +679,16 @@ class Enforcer(object):
             norm_path = '/'
         return (authz, norm_path)
 
+    @staticmethod
+    def _scope_path_matches(requested_path, allowed_path):
+        if allowed_path == '/':
+            return True
+        if requested_path == allowed_path:
+            return True
+        if allowed_path.endswith('/'):
+            return requested_path.startswith(allowed_path)
+        return requested_path.startswith(allowed_path + '/')
+
     def _validate_scp(self, value):
         if not isinstance(value, list):
             value = [value]
@@ -689,7 +699,7 @@ class Enforcer(object):
                 norm_requested_path = urltools.normalize_path(self._test_path)
             for scope in value:
                 authz, norm_path = self._check_scope(scope)
-                if (self._test_authz == authz) and 
norm_requested_path.startswith(norm_path):
+                if (self._test_authz == authz) and 
self._scope_path_matches(norm_requested_path, norm_path):
                     return True
             return False
         else:
@@ -709,7 +719,7 @@ class Enforcer(object):
             # Split on spaces
             for scope in value.split(" "):
                 authz, norm_path = self._check_scope(scope)
-                if (self._test_authz == authz) and 
norm_requested_path.startswith(norm_path):
+                if (self._test_authz == authz) and 
self._scope_path_matches(norm_requested_path, norm_path):
                     return True
             return False
         else:
@@ -718,4 +728,3 @@ class Enforcer(object):
                 authz, norm_path = self._check_scope(scope)
                 self._token_scopes.add((authz, norm_path))
             return True
-
Index: scitokens-1.8.1/tests/test_scitokens.py
===================================================================
--- scitokens-1.8.1.orig/tests/test_scitokens.py
+++ scitokens-1.8.1/tests/test_scitokens.py
@@ -193,6 +193,26 @@ class TestEnforcer(unittest.TestCase):
         with self.assertRaises(scitokens.scitokens.InvalidPathError):
             print(enf.test(self._token, "write", "~/foo"))
 
+    def test_enforce_scp_path_boundaries(self):
+        enf = scitokens.Enforcer(self._test_issuer)
+        enf.add_validator("foo", self.always_accept)
+
+        self._token["scp"] = ["read:/john"]
+        self.assertTrue(enf.test(self._token, "read", "/john"), 
msg=enf.last_failure)
+        self.assertTrue(enf.test(self._token, "read", "/john/file"), 
msg=enf.last_failure)
+        self.assertFalse(enf.test(self._token, "read", "/johnathan"), 
msg=enf.last_failure)
+        self.assertFalse(enf.test(self._token, "read", "/johnny"), 
msg=enf.last_failure)
+
+        self._token["scp"] = ["read:/john/file"]
+        self.assertFalse(enf.test(self._token, "read", "/john"), 
msg=enf.last_failure)
+
+        self._token["scp"] = ["read:/"]
+        self.assertTrue(enf.test(self._token, "read", "/arbitrary/path"), 
msg=enf.last_failure)
+
+        self._token["scp"] = ["read://john"]
+        self.assertTrue(enf.test(self._token, "read", "//john//file"), 
msg=enf.last_failure)
+        self.assertFalse(enf.test(self._token, "read", "//johnathan"), 
msg=enf.last_failure)
+
     def test_enforce_scope(self):
         """
         Test the Enforcer object.
@@ -225,6 +245,26 @@ class TestEnforcer(unittest.TestCase):
         with self.assertRaises(scitokens.scitokens.InvalidPathError):
             print(enf.test(self._token, "write", "~/foo"))
 
+    def test_enforce_scope_path_boundaries(self):
+        enf = scitokens.Enforcer(self._test_issuer)
+        enf.add_validator("foo", self.always_accept)
+
+        self._token["scope"] = "read:/john"
+        self.assertTrue(enf.test(self._token, "read", "/john"), 
msg=enf.last_failure)
+        self.assertTrue(enf.test(self._token, "read", "/john/file"), 
msg=enf.last_failure)
+        self.assertFalse(enf.test(self._token, "read", "/johnathan"), 
msg=enf.last_failure)
+        self.assertFalse(enf.test(self._token, "read", "/johnny"), 
msg=enf.last_failure)
+
+        self._token["scope"] = "read:/john/file"
+        self.assertFalse(enf.test(self._token, "read", "/john"), 
msg=enf.last_failure)
+
+        self._token["scope"] = "read:/"
+        self.assertTrue(enf.test(self._token, "read", "/arbitrary/path"), 
msg=enf.last_failure)
+
+        self._token["scope"] = "read://john"
+        self.assertTrue(enf.test(self._token, "read", "//john//file"), 
msg=enf.last_failure)
+        self.assertFalse(enf.test(self._token, "read", "//johnathan"), 
msg=enf.last_failure)
+
 
     def test_aud(self):
         """

++++++ CVE-2026-32727.patch ++++++
>From bd7e8c08690a3ed13431f04e262f00cd0fb8a9d3 Mon Sep 17 00:00:00 2001
From: Derek Weitzel <[email protected]>
Date: Fri, 13 Mar 2026 14:33:38 -0500
Subject: [PATCH 1/3] Enhance path traversal protection in Enforcer with
 additional checks and unit tests

---
 src/scitokens/scitokens.py | 29 +++++++++++-
 tests/test_scitokens.py    | 95 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 122 insertions(+), 2 deletions(-)

Index: scitokens-1.8.1/src/scitokens/scitokens.py
===================================================================
--- scitokens-1.8.1.orig/src/scitokens/scitokens.py
+++ scitokens-1.8.1/src/scitokens/scitokens.py
@@ -10,6 +10,7 @@ import time
 
 import os
 import jwt
+import re
 from . import urltools
 import logging
 
@@ -462,7 +463,7 @@ class InvalidPathError(EnforcementError)
     Test paths must be absolute paths (start with '/')
     """
 
-class InvalidAuthorizationResource(EnforcementError):
+class InvalidAuthorizationResource(ValidationFailure, EnforcementError):
     """
     A scope was encountered with an invalid authorization.
 
@@ -674,12 +675,36 @@ class Enforcer(object):
             path = info[1]
             if not path.startswith("/"):
                 raise InvalidAuthorizationResource("Token contains a relative 
path in scope")
-            norm_path = urltools.normalize_path(path)
+            norm_path = self._normalize_scope_path(path)
         else:
             norm_path = '/'
         return (authz, norm_path)
 
     @staticmethod
+    def _decode_scope_path_segment(segment):
+        normalized_segment = re.sub(
+            r"%([0-9A-Fa-f]{2})",
+            lambda match: "%" + match.group(1).lower(),
+            segment,
+        )
+        return urltools.unquote(normalized_segment, exceptions='/?+#')
+
+    @classmethod
+    def _normalize_scope_path(cls, path):
+        for segment in path.split("/"):
+            if cls._decode_scope_path_segment(segment) == "..":
+                raise InvalidAuthorizationResource("Token contains path 
traversal in scope")
+        normalized = urltools.normalize_path(path)
+        # Defense-in-depth: verify the normalized path hasn't escaped root
+        # via double-encoding or other tricks that bypass the segment check.
+        if not normalized.startswith("/"):
+            raise InvalidAuthorizationResource("Token contains path traversal 
in scope")
+        for segment in normalized.split("/"):
+            if segment == "..":
+                raise InvalidAuthorizationResource("Token contains path 
traversal in scope")
+        return normalized
+
+    @staticmethod
     def _scope_path_matches(requested_path, allowed_path):
         if allowed_path == '/':
             return True
Index: scitokens-1.8.1/tests/test_scitokens.py
===================================================================
--- scitokens-1.8.1.orig/tests/test_scitokens.py
+++ scitokens-1.8.1/tests/test_scitokens.py
@@ -213,6 +213,28 @@ class TestEnforcer(unittest.TestCase):
         self.assertTrue(enf.test(self._token, "read", "//john//file"), 
msg=enf.last_failure)
         self.assertFalse(enf.test(self._token, "read", "//johnathan"), 
msg=enf.last_failure)
 
+    def test_enforce_scp_path_traversal(self):
+        enf = scitokens.Enforcer(self._test_issuer)
+        enf.add_validator("foo", self.always_accept)
+
+        bad_scopes = [
+            ("read:/home/user1/..", "/home/user2"),
+            ("read:/anything/..", "/etc/passwd"),
+            ("read:/foo/%2e%2e/bar", "/bar"),
+            ("read:/foo/.%2e/bar", "/bar"),
+            ("read:/foo/%2e./bar", "/bar"),
+            ("read:/foo/%2E%2E/bar", "/bar"),
+        ]
+
+        for scope, requested_path in bad_scopes:
+            self._token["scp"] = scope
+            self.assertFalse(enf.test(self._token, "read", requested_path), 
msg=enf.last_failure)
+            self.assertIn("path traversal", enf.last_failure)
+
+        self._token["scp"] = "read:/foo/%2e%2e/bar"
+        with 
self.assertRaises(scitokens.scitokens.InvalidAuthorizationResource):
+            enf.generate_acls(self._token)
+
     def test_enforce_scope(self):
         """
         Test the Enforcer object.
@@ -265,6 +287,74 @@ class TestEnforcer(unittest.TestCase):
         self.assertTrue(enf.test(self._token, "read", "//john//file"), 
msg=enf.last_failure)
         self.assertFalse(enf.test(self._token, "read", "//johnathan"), 
msg=enf.last_failure)
 
+    def test_enforce_scope_path_traversal(self):
+        enf = scitokens.Enforcer(self._test_issuer)
+        enf.add_validator("foo", self.always_accept)
+
+        bad_scopes = [
+            ("read:/home/user1/..", "/home/user2"),
+            ("read:/anything/..", "/etc/passwd"),
+            ("read:/foo/%2e%2e/bar", "/bar"),
+            ("read:/foo/.%2e/bar", "/bar"),
+            ("read:/foo/%2e./bar", "/bar"),
+            ("read:/foo/%2E%2E/bar", "/bar"),
+        ]
+
+        for scope, requested_path in bad_scopes:
+            self._token["scope"] = scope
+            self.assertFalse(enf.test(self._token, "read", requested_path), 
msg=enf.last_failure)
+            self.assertIn("path traversal", enf.last_failure)
+
+        self._token["scope"] = "read:/foo/%2e%2e/bar"
+        with 
self.assertRaises(scitokens.scitokens.InvalidAuthorizationResource):
+            enf.generate_acls(self._token)
+
+    def test_enforce_scope_path_traversal_double_encoded(self):
+        """
+        Defense-in-depth: double-encoded and other encoding variations must
+        not allow path traversal even if the pre-normalization segment check
+        doesn't catch them.
+        """
+        enf = scitokens.Enforcer(self._test_issuer)
+        enf.add_validator("foo", self.always_accept)
+
+        # Double-encoded '..' (%252e%252e decodes once to %2e%2e)
+        # These should either be caught or treated as opaque literal segments
+        # — never resolved to actual '..' traversal.
+        double_encoded_scopes = [
+            "read:/foo/%252e%252e/bar",
+            "read:/foo/%252E%252E/bar",
+            "read:/foo/%252e./bar",
+            "read:/foo/.%252e/bar",
+        ]
+        for scope in double_encoded_scopes:
+            self._token["scope"] = scope
+            # Must not grant access to /bar (the traversed path)
+            self.assertFalse(
+                enf.test(self._token, "read", "/bar"),
+                msg="Scope {!r} should not grant access to /bar".format(scope),
+            )
+
+    def test_normalize_scope_path_rejects_traversal(self):
+        """
+        Test that _normalize_scope_path rejects traversal and encoded
+        traversal paths, and still accepts benign normalized paths.
+        """
+        enforcer_cls = scitokens.scitokens.Enforcer
+
+        # These should all be rejected
+        for bad_path in ["/a/../b", "/a/%2e%2e/b", "/a/.%2e/b", "/a/%2e./b"]:
+            with self.assertRaises(
+                scitokens.scitokens.InvalidAuthorizationResource,
+                msg="Path {!r} should be rejected".format(bad_path),
+            ):
+                enforcer_cls._normalize_scope_path(bad_path)
+
+        # Valid paths must still work
+        for good_path in ["/a/b/c", "/a/b/../c".replace("..", "safe"), "/", 
"/a/"]:
+            result = enforcer_cls._normalize_scope_path(good_path)
+            self.assertTrue(result.startswith("/"))
+
 
     def test_aud(self):
         """
@@ -373,6 +463,10 @@ class TestEnforcer(unittest.TestCase):
         with 
self.assertRaises(scitokens.scitokens.InvalidAuthorizationResource):
             print(enf.generate_acls(self._token))
 
+        self._token['scope'] = 'read:/foo/%2e%2e/bar'
+        with 
self.assertRaises(scitokens.scitokens.InvalidAuthorizationResource):
+            enf.generate_acls(self._token)
+
     def test_sub(self):
         """
         Verify that tokens with the `sub` set are accepted.

Reply via email to