nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: PyCQA/bandit
Pull request #905 (2022-05-24 20:28:56)

Add another bad example of yaml load
The yaml module supports passing the Loader of choice.
Passing yaml.Loader is considered unsafe. This commit
adds that example and ensures Bandit detects it.

Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com>


Patch:
diff --git a/examples/yaml_load.py b/examples/yaml_load.py
index 346818d..21d8797 100644
--- a/examples/yaml_load.py
+++ b/examples/yaml_load.py
@@ -16,3 +16,5 @@ def test_yaml_load():
 def test_json_load():
     # no issue should be found
     j = json.load("{}")
+
+yaml.load("{}", Loader=yaml.Loader)


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index adad9f5..d39ee79 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -492,8 +492,8 @@ class FunctionalTests(testtools.TestCase):
     def test_yaml(self):
         """Test for `yaml.load`."""
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 2, "HIGH": 0},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2},
         }
         self.check_example("yaml_load.py", expect)
Repository: PyCQA/bandit
Pull request #923 (2022-07-05 17:43:57)

Fix reading the number argument from config file
When passing the "number" option from the INI file we did
not take into account to store its value as an integer
(when that value is not None).

Resolves: #922


Patch:
diff --git a/bandit/blacklists/calls.py b/bandit/blacklists/calls.py
index c81e1cd..079da90 100644
--- a/bandit/blacklists/calls.py
+++ b/bandit/blacklists/calls.py
@@ -32,6 +32,9 @@ deserialize untrusted data, possible security issue.
 |      |                     | - dill.Unpickler                   |           |
 |      |                     | - shelve.open                      |           |
 |      |                     | - shelve.DbfilenameShelf           |           |
+|      |                     | - jsonpickle.decode                |           |
+|      |                     | - jsonpickle.unpickler.decode      |           |
+|      |                     | - jsonpickle.unpickler.Unpickler   |           |
 +------+---------------------+------------------------------------+-----------+
 
 B302: marshal
@@ -352,6 +355,9 @@ def gen_blacklist():
                 "dill.Unpickler",
                 "shelve.open",
                 "shelve.DbfilenameShelf",
+                "jsonpickle.decode",
+                "jsonpickle.unpickler.decode",
+                "jsonpickle.unpickler.Unpickler",
             ],
             "Pickle and modules that wrap it can be unsafe when used to "
             "deserialize untrusted data, possible security issue.",
diff --git a/bandit/cli/main.py b/bandit/cli/main.py
index d5eb57d..4758885 100644
--- a/bandit/cli/main.py
+++ b/bandit/cli/main.py
@@ -512,7 +512,7 @@ def main():
         args.context_lines = _log_option_source(
             parser.get_default("context_lines"),
             args.context_lines,
-            ini_options.get("number"),
+            int(ini_options.get("number") or 0) or None,
             "max code lines output for issue",
         )
 
diff --git a/examples/jsonpickle.py b/examples/jsonpickle.py
new file mode 100644
index 0000000..e8aa290
--- /dev/null
+++ b/examples/jsonpickle.py
@@ -0,0 +1,10 @@
+import jsonpickle
+
+
+pick = jsonpickle.encode({'a': 'b', 'c': 'd'})
+
+print(jsonpickle.decode(pick))
+
+print(jsonpickle.unpickler.decode(pick))
+
+print(jsonpickle.unpickler.Unpickler().restore(pick))


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 7b83d79..2005d1e 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -369,6 +369,14 @@ class FunctionalTests(testtools.TestCase):
         }
         self.check_example("shelve_open.py", expect)
 
+    def test_jsonpickle(self):
+        """Test for the `jsonpickle` module."""
+        expect = {
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3},
+        }
+        self.check_example("jsonpickle.py", expect)
+
     def test_popen_wrappers(self):
         """Test the `popen2` and `commands` modules."""
         expect = {
Repository: PyCQA/bandit
Pull request #925 (2022-07-08 03:38:40)

Docs for request without timeout has dead link
The see also section refers to an invalid URL. This change updates
with the latest known reference.

Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com>


Patch:
diff --git a/bandit/blacklists/calls.py b/bandit/blacklists/calls.py
index 079da90..977dbcd 100644
--- a/bandit/blacklists/calls.py
+++ b/bandit/blacklists/calls.py
@@ -35,6 +35,7 @@ deserialize untrusted data, possible security issue.
 |      |                     | - jsonpickle.decode                |           |
 |      |                     | - jsonpickle.unpickler.decode      |           |
 |      |                     | - jsonpickle.unpickler.Unpickler   |           |
+|      |                     | - pandas.read_pickle               |           |
 +------+---------------------+------------------------------------+-----------+
 
 B302: marshal
@@ -358,6 +359,7 @@ def gen_blacklist():
                 "jsonpickle.decode",
                 "jsonpickle.unpickler.decode",
                 "jsonpickle.unpickler.Unpickler",
+                "pandas.read_pickle",
             ],
             "Pickle and modules that wrap it can be unsafe when used to "
             "deserialize untrusted data, possible security issue.",
diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py
index d39fd8f..9aa80bf 100644
--- a/bandit/plugins/request_without_timeout.py
+++ b/bandit/plugins/request_without_timeout.py
@@ -38,7 +38,7 @@ Bandit will return a MEDIUM severity error.
 
 .. seealso::
 
- - https://2.python-requests.org/en/master/user/quickstart/#timeouts
+ - https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
 
 .. versionadded:: 1.7.5
 
diff --git a/examples/pandas_read_pickle.py b/examples/pandas_read_pickle.py
new file mode 100644
index 0000000..61174f6
--- /dev/null
+++ b/examples/pandas_read_pickle.py
@@ -0,0 +1,12 @@
+import pickle
+import pandas as pd
+
+
+df = pd.DataFrame(
+    {
+        "col_A": [1, 2]
+    }
+)
+pick = pickle.dumps(df)
+
+print(pd.read_pickle(pick))


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 2005d1e..1d1bd7e 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -377,6 +377,14 @@ class FunctionalTests(testtools.TestCase):
         }
         self.check_example("jsonpickle.py", expect)
 
+    def test_pandas_read_pickle(self):
+        """Test for the `pandas.read_pickle` module."""
+        expect = {
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 1, "HIGH": 0},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2},
+        }
+        self.check_example("pandas_read_pickle.py", expect)
+
     def test_popen_wrappers(self):
         """Test the `popen2` and `commands` modules."""
         expect = {
Repository: PyCQA/bandit
Pull request #928 (2022-07-10 19:03:04)

Check for deprecated TLS 1.1
The IETF officially deprecated TLS 1.1 in March 2021. As such,
Bandit should now check for and warn of its use.

https://datatracker.ietf.org/doc/rfc8996/

Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com>


Patch:
diff --git a/bandit/plugins/insecure_ssl_tls.py b/bandit/plugins/insecure_ssl_tls.py
index 80f2e5c..319abcf 100644
--- a/bandit/plugins/insecure_ssl_tls.py
+++ b/bandit/plugins/insecure_ssl_tls.py
@@ -22,6 +22,8 @@ def gen_config(name):
                 "PROTOCOL_TLSv1",  # strict option
                 "SSLv3_METHOD",  # strict option
                 "TLSv1_METHOD",
+                "PROTOCOL_TLSv1_1",
+                "TLSv1_1_METHOD",
             ]
         }  # strict option
 
@@ -106,6 +108,9 @@ def ssl_with_bad_version(context, config):
     .. versionchanged:: 1.7.3
         CWE information added
 
+    .. versionchanged:: 1.7.5
+        Added TLS 1.1
+
     """
     bad_ssl_versions = get_bad_proto_versions(config)
     if context.call_function_name_qual == "ssl.wrap_socket":
@@ -196,6 +201,9 @@ def ssl_with_bad_defaults(context, config):
     .. versionchanged:: 1.7.3
         CWE information added
 
+    .. versionchanged:: 1.7.5
+        Added TLS 1.1
+
     """
 
     bad_ssl_versions = get_bad_proto_versions(config)
diff --git a/examples/ssl-insecure-version.py b/examples/ssl-insecure-version.py
index bd6f659..80d6e48 100644
--- a/examples/ssl-insecure-version.py
+++ b/examples/ssl-insecure-version.py
@@ -20,6 +20,13 @@ herp_derp(ssl_version=ssl.PROTOCOL_TLSv1)
 herp_derp(method=SSL.SSLv3_METHOD)
 herp_derp(method=SSL.TLSv1_METHOD)
 
+ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_1)
+SSL.Context(method=SSL.TLSv1_1_METHOD)
+
+herp_derp(ssl_version=ssl.PROTOCOL_TLSv1_1)
+herp_derp(method=SSL.TLSv1_1_METHOD)
+
+
 ssl.wrap_socket()
 
 def open_ssl_socket(version=ssl.PROTOCOL_SSLv2):
@@ -31,6 +38,9 @@ def open_ssl_socket(version=SSL.SSLv2_METHOD):
 def open_ssl_socket(version=SSL.SSLv23_METHOD):
     pass
 
-# this one will pass ok
 def open_ssl_socket(version=SSL.TLSv1_1_METHOD):
     pass
+
+# this one will pass ok
+def open_ssl_socket(version=SSL.TLSv1_2_METHOD):
+    pass


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index cb48377..8b8088e 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -454,12 +454,8 @@ class FunctionalTests(testtools.TestCase):
     def test_ssl_insecure_version(self):
         """Test for insecure SSL protocol versions."""
         expect = {
-            "SEVERITY": {"LOW": 1, "MEDIUM": 10, "HIGH": 7},
-            "CONFIDENCE": {"LOW": 0, "MEDIUM": 11, "HIGH": 7},
-        }
-        expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 10, "HIGH": 7},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 11, "HIGH": 7},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 13, "HIGH": 9},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 14, "HIGH": 9},
         }
         self.check_example("ssl-insecure-version.py", expect)
Repository: PyCQA/bandit
Pull request #1064 (2023-09-25 18:31:08)

fix(plugins/B507): also detect class instances
`paramiko` supports passing both a class and a class instance for the policy in `set_missing_host_key_policy`
(https://github.com/paramiko/paramiko/blob/8e389c77660c5cdae3069b478665427d23012853/paramiko/client.py#L171-L191). This updates B507 to account for both styles.


Patch:
diff --git a/bandit/plugins/ssh_no_host_key_verification.py b/bandit/plugins/ssh_no_host_key_verification.py
index 2f43903..e8edaf9 100644
--- a/bandit/plugins/ssh_no_host_key_verification.py
+++ b/bandit/plugins/ssh_no_host_key_verification.py
@@ -35,6 +35,8 @@ verification is disabled, Bandit will return a HIGH severity error.
     CWE information added
 
 """
+import ast
+
 import bandit
 from bandit.core import issue
 from bandit.core import test_properties as test
@@ -46,11 +48,17 @@ def ssh_no_host_key_verification(context):
     if (
         context.is_module_imported_like("paramiko")
         and context.call_function_name == "set_missing_host_key_policy"
+        and context.node.args
     ):
-        if context.call_args and context.call_args[0] in [
-            "AutoAddPolicy",
-            "WarningPolicy",
-        ]:
+        policy_argument = context.node.args[0]
+
+        policy_argument_value = None
+        if isinstance(policy_argument, ast.Attribute):
+            policy_argument_value = policy_argument.attr
+        elif isinstance(policy_argument, ast.Call):
+            policy_argument_value = policy_argument.func.attr
+
+        if policy_argument_value in ["AutoAddPolicy", "WarningPolicy"]:
             return bandit.Issue(
                 severity=bandit.HIGH,
                 confidence=bandit.MEDIUM,
diff --git a/examples/no_host_key_verification.py b/examples/no_host_key_verification.py
index ff12b34..2e092fe 100644
--- a/examples/no_host_key_verification.py
+++ b/examples/no_host_key_verification.py
@@ -3,3 +3,5 @@ from paramiko import client
 ssh_client = client.SSHClient()
 ssh_client.set_missing_host_key_policy(client.AutoAddPolicy)
 ssh_client.set_missing_host_key_policy(client.WarningPolicy)
+ssh_client.set_missing_host_key_policy(client.AutoAddPolicy())
+ssh_client.set_missing_host_key_policy(client.WarningPolicy())


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 7835e74..6917462 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -543,8 +543,8 @@ class FunctionalTests(testtools.TestCase):
     def test_host_key_verification(self):
         """Test for ignoring host key verification."""
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 2, "HIGH": 0},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 0},
         }
         self.check_example("no_host_key_verification.py", expect)
Repository: PyCQA/bandit
Pull request #1096 (2024-01-18 22:43:51)

Add random.randbytes to blacklist calls
In Python 3.9, the random module added new function randbytes(n). This function shouldn't be used for any cryptographic operations. As the doc recommends, use secrets.token_bytes() instead.

https://docs.python.org/3/library/random.html#random.randbytes


Patch:
diff --git a/bandit/blacklists/calls.py b/bandit/blacklists/calls.py
index 9e7ec74..d69f5dd 100644
--- a/bandit/blacklists/calls.py
+++ b/bandit/blacklists/calls.py
@@ -198,6 +198,7 @@ https://docs.python.org/library/secrets.html
 |      |                     | - random.choices                   |           |
 |      |                     | - random.uniform                   |           |
 |      |                     | - random.triangular                |           |
+|      |                     | - random.randbytes                 |           |
 +------+---------------------+------------------------------------+-----------+
 
 B312: telnetlib
@@ -523,6 +524,7 @@ def gen_blacklist():
                 "random.choices",
                 "random.uniform",
                 "random.triangular",
+                "random.randbytes",
             ],
             "Standard pseudo-random generators are not suitable for "
             "security/cryptographic purposes.",
diff --git a/examples/random_module.py b/examples/random_module.py
index 2bf80d0..224f251 100644
--- a/examples/random_module.py
+++ b/examples/random_module.py
@@ -10,6 +10,7 @@ bad = random.choice()
 bad = random.choices()
 bad = random.uniform()
 bad = random.triangular()
+bad = random.randbytes()
 
 good = os.urandom()
 good = random.SystemRandom()


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 8469943..a230dc3 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -396,8 +396,8 @@ class FunctionalTests(testtools.TestCase):
     def test_random_module(self):
         """Test for the `random` module."""
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 8, "MEDIUM": 0, "HIGH": 0},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 8},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 9, "MEDIUM": 0, "HIGH": 0},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9},
         }
         self.check_example("random_module.py", expect)
Repository: PyCQA/bandit
Pull request #1116 (2024-03-06 09:34:39)

[B605] Add functions that are vulnerable to shell injection.
`subprocess.getstatusoutput()` will call `check_output(cmd, shell=True)` [source code](https://github.com/python/cpython/blob/22ccf13b332902142fe0c52c593f9efc152c7761/Lib/subprocess.py#L675), so I think extending the scan scope is proper.

shihai1991: > Can you also please add to [examples/subprocess_shell.py](https://github.com/PyCQA/bandit/blob/main/examples/subprocess_shell.py) and update the corresponding test case with the new issue counts. Thanks!

Sure, I will update it soon.

Patch:
diff --git a/bandit/plugins/injection_shell.py b/bandit/plugins/injection_shell.py
index c138a52..5bd9c96 100644
--- a/bandit/plugins/injection_shell.py
+++ b/bandit/plugins/injection_shell.py
@@ -49,6 +49,8 @@ def gen_config(name):
                 "popen2.Popen4",
                 "commands.getoutput",
                 "commands.getstatusoutput",
+                "subprocess.getoutput",
+                "subprocess.getstatusoutput",
             ],
             # Start a process with a function that is not vulnerable to shell
             # injection.
@@ -447,6 +449,8 @@ def start_process_with_a_shell(context, config):
                 - popen2.Popen4
                 - commands.getoutput
                 - commands.getstatusoutput
+                - subprocess.getoutput
+                - subprocess.getstatusoutput
 
     :Example:
 
diff --git a/examples/subprocess_shell.py b/examples/subprocess_shell.py
index 049ed05..d8076d5 100644
--- a/examples/subprocess_shell.py
+++ b/examples/subprocess_shell.py
@@ -26,6 +26,9 @@ subprocess.check_call('/bin/ls -l', shell=True)
 subprocess.check_output(['/bin/ls', '-l'])
 subprocess.check_output('/bin/ls -l', shell=True)
 
+subprocess.getoutput('/bin/ls -l')
+subprocess.getstatusoutput('/bin/ls -l')
+
 subprocess.run(['/bin/ls', '-l'])
 subprocess.run('/bin/ls -l', shell=True)
 


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 401c46f..2d27cdd 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -493,8 +493,8 @@ class FunctionalTests(testtools.TestCase):
     def test_subprocess_shell(self):
         """Test for `subprocess.Popen` with `shell=True`."""
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 21, "MEDIUM": 1, "HIGH": 11},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 32},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 23, "MEDIUM": 1, "HIGH": 11},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 34},
         }
         self.check_example("subprocess_shell.py", expect)
Repository: PyCQA/bandit
Pull request #1146 (2024-06-12 20:00:13)

Guard against empty call argument list
Although probably uncommon, it is possible to pass an empty list to one of subprocess functions. If this is done, the injection_shell plugin raises an IndexError while checking the contents of the list argument given.

The fix is to simply check for a non-empty list. Test case was also added.

Fixes: #1141


Patch:
diff --git a/bandit/plugins/injection_shell.py b/bandit/plugins/injection_shell.py
index 5bd9c96..2293683 100644
--- a/bandit/plugins/injection_shell.py
+++ b/bandit/plugins/injection_shell.py
@@ -683,7 +683,7 @@ def start_process_with_partial_path(context, config):
         ):
             node = context.node.args[0]
             # some calls take an arg list, check the first part
-            if isinstance(node, ast.List):
+            if isinstance(node, ast.List) and node.elts:
                 node = node.elts[0]
 
             # make sure the param is a string literal and not a var name
diff --git a/examples/subprocess_shell.py b/examples/subprocess_shell.py
index d8076d5..38944d5 100644
--- a/examples/subprocess_shell.py
+++ b/examples/subprocess_shell.py
@@ -25,6 +25,7 @@ subprocess.check_call('/bin/ls -l', shell=True)
 
 subprocess.check_output(['/bin/ls', '-l'])
 subprocess.check_output('/bin/ls -l', shell=True)
+subprocess.check_output([], stdout=None)
 
 subprocess.getoutput('/bin/ls -l')
 subprocess.getstatusoutput('/bin/ls -l')


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 85db6ab..fd96796 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -492,8 +492,8 @@ class FunctionalTests(testtools.TestCase):
     def test_subprocess_shell(self):
         """Test for `subprocess.Popen` with `shell=True`."""
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 23, "MEDIUM": 1, "HIGH": 11},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 34},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 24, "MEDIUM": 1, "HIGH": 11},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 35},
         }
         self.check_example("subprocess_shell.py", expect)
Repository: kurator-dev/kurator
Pull request #267 (2023-04-17 09:15:53)

clusteroperator: infraType is immutable
**What type of PR is this?**

<!--
Add one of the following kinds:

/kind api-change
/kind bug
/kind cleanup
/kind deprecation
/kind design
/kind documentation
/kind failing-test
/kind feature
/kind flake

-->

/kind bug

**What this PR does / why we need it**:

infraType is immutable.

**Which issue(s) this PR fixes**:
Fixes #

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required.
-->
```release-note

```




Patch:
diff --git a/pkg/webhooks/cluster_webhook.go b/pkg/webhooks/cluster_webhook.go
index c0ea765..f23c0aa 100644
--- a/pkg/webhooks/cluster_webhook.go
+++ b/pkg/webhooks/cluster_webhook.go
@@ -197,13 +197,24 @@ func (wh *ClusterWebhook) validate(in *v1.Cluster) error {
 	return nil
 }
 
-func (wh *ClusterWebhook) ValidateUpdate(_ context.Context, obj runtime.Object, _ runtime.Object) error {
-	in, ok := obj.(*v1.Cluster)
+func (wh *ClusterWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) error {
+	oldCluster, ok := oldObj.(*v1.Cluster)
 	if !ok {
-		return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", oldObj))
 	}
 
-	return wh.validate(in)
+	newCluster, ok := newObj.(*v1.Cluster)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", newObj))
+	}
+
+	if oldCluster.Spec.InfraType != newCluster.Spec.InfraType {
+		return apierrors.NewInvalid(v1.SchemeGroupVersion.WithKind("Cluster").GroupKind(), newCluster.Name, field.ErrorList{
+			field.Forbidden(field.NewPath("spec", "infraType"), "field is immutable"),
+		})
+	}
+
+	return wh.validate(newCluster)
 }
 
 func (wh *ClusterWebhook) ValidateDelete(_ context.Context, obj runtime.Object) error {


Test patch:
diff --git a/pkg/webhooks/cluster_webhook_test.go b/pkg/webhooks/cluster_webhook_test.go
index 674da3b..3b41176 100644
--- a/pkg/webhooks/cluster_webhook_test.go
+++ b/pkg/webhooks/cluster_webhook_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package webhooks
 
 import (
+	"context"
 	"io/fs"
 	"os"
 	"path"
@@ -25,6 +26,7 @@ import (
 
 	. "github.com/onsi/gomega"
 	"github.com/stretchr/testify/assert"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"sigs.k8s.io/yaml"
 
 	v1 "kurator.dev/kurator/pkg/apis/cluster/v1alpha1"
@@ -86,6 +88,20 @@ func TestInvalidClusterValidation(t *testing.T) {
 	}
 }
 
+func TestUpdateClusterInfraType(t *testing.T) {
+	wh := &ClusterWebhook{}
+	oldCluster, err := readCluster("../../examples/cluster/quickstart.yaml")
+	assert.NoError(t, err)
+
+	newCluster := oldCluster.DeepCopy()
+	newCluster.Spec.InfraType = "aws1"
+
+	err = wh.ValidateUpdate(context.TODO(), oldCluster, newCluster)
+	if !apierrors.IsInvalid(err) {
+		t.Errorf("Expect an invalid error, got %v", err)
+	}
+}
+
 func readCluster(filename string) (*v1.Cluster, error) {
 	b, err := os.ReadFile(filename)
 	if err != nil {