diff --git a/powershell/ql/lib/semmle/code/powershell/security/UnsafeDeserializationCustomizations.qll b/powershell/ql/lib/semmle/code/powershell/security/UnsafeDeserializationCustomizations.qll index 4615161364fb..f75ce4ade5e6 100644 --- a/powershell/ql/lib/semmle/code/powershell/security/UnsafeDeserializationCustomizations.qll +++ b/powershell/ql/lib/semmle/code/powershell/security/UnsafeDeserializationCustomizations.qll @@ -102,6 +102,31 @@ module UnsafeDeserialization { typeName = "system.resources.resxresourcereader" } + /** + * An argument to Import-Clixml or ConvertFrom-CliXml, which deserializes CLIXML + * and can trigger gadget chains leading to code execution. + */ + class CliXmlDeserializationSink extends Sink { + string cmdletName; + + CliXmlDeserializationSink() { + exists(DataFlow::CallNode call | + call.matchesName(cmdletName) and + ( + this = call.getAnArgument() + or + this.asExpr().(CfgNodes::ExprNodes::PipelineArgumentCfgNode).getCall() = + call.getCallNode() + ) + | + cmdletName = "Import-Clixml" or + cmdletName = "ConvertFrom-CliXml" + ) + } + + override string getSinkType() { result = "call to " + cmdletName } + } + /** * A BinaryFormatter deserialization method call, including Deserialize, UnsafeDeserialize, * and UnsafeDeserializeMethodResponse. diff --git a/powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.qhelp b/powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.qhelp new file mode 100644 index 000000000000..9f18b5a084cf --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.qhelp @@ -0,0 +1,40 @@ + + + +

+When deriving cryptographic keys from passwords using Rfc2898DeriveBytes (PBKDF2), +both the iteration count and hash algorithm must be configured securely. +An insufficient iteration count or a weak hash algorithm makes the derived key +vulnerable to brute-force attacks. +

+
+ + +

+Always specify at least 100,000 iterations and use SHA-256 or a stronger hash algorithm +(SHA-384, SHA-512) when creating an Rfc2898DeriveBytes instance or calling the +static Pbkdf2 method. +

+
+ + +

The following example shows insecure usage with default settings:

+ +

The following example shows secure usage with adequate iterations and a strong hash:

+ +
+ + +
  • + OWASP: Password Storage Cheat Sheet +
  • +
  • + Microsoft: Rfc2898DeriveBytes Class +
  • +
  • + CWE-327: Use of a Broken or Risky Cryptographic Algorithm +
  • +
    +
    diff --git a/powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.ql b/powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.ql new file mode 100644 index 000000000000..e0a6a16d7b80 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.ql @@ -0,0 +1,168 @@ +/** + * @name Weak key derivation function configuration + * @description Rfc2898DeriveBytes (PBKDF2) should use at least 100,000 iterations + * and a hash algorithm of SHA-256 or stronger to resist brute-force attacks. + * @kind problem + * @problem.severity error + * @security-severity 7.5 + * @precision high + * @id powershell/weak-kdf-configuration + * @tags security + * external/cwe/cwe-327 + * external/cwe/cwe-328 + * cryptography + */ + +import powershell +import semmle.code.powershell.ApiGraphs +import semmle.code.powershell.dataflow.DataFlow + +private int minIterationCount() { result = 100000 } + +/** + * An instantiation of Rfc2898DeriveBytes via New-Object or [Type]::new(). + */ +class Rfc2898DeriveBytesCreation extends DataFlow::CallNode { + Rfc2898DeriveBytesCreation() { + this = + API::getTopLevelMember("system") + .getMember("security") + .getMember("cryptography") + .getMember("rfc2898derivebytes") + .getMember("new") + .asCall() + or + // New-Object pattern + exists(DataFlow::ObjectCreationNode oc | + oc = this and + oc.getExprNode().getExpr().(CallExpr).getAnArgument().getValue().asString().toLowerCase() = + [ + "system.security.cryptography.rfc2898derivebytes", + "rfc2898derivebytes" + ] + ) + } + + /** Gets the iteration count argument (position 2, 0-indexed), if any. */ + DataFlow::Node getIterationCountArg() { result = this.getPositionalArgument(2) } + + /** Gets the hash algorithm argument (position 3, 0-indexed), if any. */ + DataFlow::Node getHashAlgorithmArg() { result = this.getPositionalArgument(3) } + + /** Holds if the constructor has an explicit iteration count argument. */ + predicate hasIterationCountArg() { exists(this.getIterationCountArg()) } + + /** Holds if the constructor has an explicit hash algorithm argument. */ + predicate hasHashAlgorithmArg() { exists(this.getHashAlgorithmArg()) } +} + +/** + * A call to the static Rfc2898DeriveBytes.Pbkdf2 method (.NET 6+). + */ +class Pbkdf2StaticCall extends DataFlow::CallNode { + Pbkdf2StaticCall() { + this = + API::getTopLevelMember("system") + .getMember("security") + .getMember("cryptography") + .getMember("rfc2898derivebytes") + .getMember("pbkdf2") + .asCall() + } + + /** Gets the iteration count argument (position 2, 0-indexed). */ + DataFlow::Node getIterationCountArg() { result = this.getPositionalArgument(2) } + + /** Gets the hash algorithm argument (position 3, 0-indexed). */ + DataFlow::Node getHashAlgorithmArg() { result = this.getPositionalArgument(3) } +} + +/** + * Holds if `node` is an integer literal less than the minimum iteration count. + */ +private predicate isLowIterationValue(DataFlow::Node node, int value) { + value = node.asExpr().getExpr().getValue().asInt() and + value < minIterationCount() +} + +/** + * Holds if `node` references a weak hash algorithm (MD5 or SHA1). + */ +private predicate isWeakHashAlgorithm(DataFlow::Node node, string name) { + // [HashAlgorithmName]::MD5 or [HashAlgorithmName]::SHA1 + node = + API::getTopLevelMember("system") + .getMember("security") + .getMember("cryptography") + .getMember("hashalgorithmname") + .getMember(name) + .asSource() and + name = ["md5", "sha1"] + or + // String literal "MD5" or "SHA1" + exists(string s | + s = node.asExpr().getExpr().getValue().asString().toLowerCase() and + s = ["md5", "sha1"] and + name = s + ) +} + +/** + * Gets a human-readable name for the weak hash algorithm. + */ +private string prettyHashName(string name) { + name = "md5" and result = "MD5" + or + name = "sha1" and result = "SHA1" +} + +from DataFlow::CallNode call, string message +where + // Case 1: Rfc2898DeriveBytes created without specifying iteration count + call instanceof Rfc2898DeriveBytesCreation and + not call.(Rfc2898DeriveBytesCreation).hasIterationCountArg() and + message = + "Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least " + + minIterationCount().toString() + " iterations." + or + // Case 2: Rfc2898DeriveBytes created with low iteration count + exists(int value | + call instanceof Rfc2898DeriveBytesCreation and + isLowIterationValue(call.(Rfc2898DeriveBytesCreation).getIterationCountArg(), value) and + message = + "Rfc2898DeriveBytes uses iteration count of " + value.toString() + + ", which is below the minimum of " + minIterationCount().toString() + "." + ) + or + // Case 3: Rfc2898DeriveBytes created without specifying hash algorithm (defaults to SHA1) + call instanceof Rfc2898DeriveBytesCreation and + not call.(Rfc2898DeriveBytesCreation).hasHashAlgorithmArg() and + message = "Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger." + or + // Case 4: Rfc2898DeriveBytes created with weak hash algorithm + exists(string name | + call instanceof Rfc2898DeriveBytesCreation and + isWeakHashAlgorithm(call.(Rfc2898DeriveBytesCreation).getHashAlgorithmArg(), name) and + message = + "Rfc2898DeriveBytes uses weak hash algorithm " + prettyHashName(name) + + ". Use SHA-256 or stronger." + ) + or + // Case 5: Pbkdf2 static method with low iteration count + exists(int value | + call instanceof Pbkdf2StaticCall and + isLowIterationValue(call.(Pbkdf2StaticCall).getIterationCountArg(), value) and + message = + "Rfc2898DeriveBytes.Pbkdf2 uses iteration count of " + value.toString() + + ", which is below the minimum of " + minIterationCount().toString() + "." + ) + or + // Case 6: Pbkdf2 static method with weak hash algorithm + exists(string name | + call instanceof Pbkdf2StaticCall and + isWeakHashAlgorithm(call.(Pbkdf2StaticCall).getHashAlgorithmArg(), name) and + message = + "Rfc2898DeriveBytes.Pbkdf2 uses weak hash algorithm " + prettyHashName(name) + + ". Use SHA-256 or stronger." + ) +select call, message diff --git a/powershell/ql/src/queries/security/cwe-327/examples/WeakKDFConfigurationBad.ps1 b/powershell/ql/src/queries/security/cwe-327/examples/WeakKDFConfigurationBad.ps1 new file mode 100644 index 000000000000..20865588dff1 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-327/examples/WeakKDFConfigurationBad.ps1 @@ -0,0 +1,8 @@ +# BAD: Default iteration count (1000) and default hash algorithm (SHA1) +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt) + +# BAD: Low iteration count +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 1000) + +# BAD: Weak hash algorithm +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA1) diff --git a/powershell/ql/src/queries/security/cwe-327/examples/WeakKDFConfigurationGood.ps1 b/powershell/ql/src/queries/security/cwe-327/examples/WeakKDFConfigurationGood.ps1 new file mode 100644 index 000000000000..dbea0c1c062f --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-327/examples/WeakKDFConfigurationGood.ps1 @@ -0,0 +1,5 @@ +# GOOD: 100,000+ iterations with SHA-256 +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 600000, [System.Security.Cryptography.HashAlgorithmName]::SHA256) + +# GOOD: Static Pbkdf2 with strong configuration +$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 600000, "SHA256", 32) diff --git a/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.expected b/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.expected new file mode 100644 index 000000000000..562d68253afa --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.expected @@ -0,0 +1,21 @@ +| WeakKDFConfiguration.ps1:7:8:7:79 | Call to new | Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least 100000 iterations. | +| WeakKDFConfiguration.ps1:7:8:7:79 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:11:8:11:85 | Call to new | Rfc2898DeriveBytes uses iteration count of 1000, which is below the minimum of 100000. | +| WeakKDFConfiguration.ps1:11:8:11:85 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:14:8:14:86 | Call to new | Rfc2898DeriveBytes uses iteration count of 10000, which is below the minimum of 100000. | +| WeakKDFConfiguration.ps1:14:8:14:86 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:17:8:17:86 | Call to new | Rfc2898DeriveBytes uses iteration count of 50000, which is below the minimum of 100000. | +| WeakKDFConfiguration.ps1:17:8:17:86 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:21:8:21:87 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:25:8:25:143 | Call to new | Rfc2898DeriveBytes uses weak hash algorithm SHA1. Use SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:28:8:28:142 | Call to new | Rfc2898DeriveBytes uses weak hash algorithm MD5. Use SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:32:8:32:89 | Call to new-object | Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least 100000 iterations. | +| WeakKDFConfiguration.ps1:32:8:32:89 | Call to new-object | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:36:8:36:83 | Call to new-object | Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least 100000 iterations. | +| WeakKDFConfiguration.ps1:36:8:36:83 | Call to new-object | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:40:8:40:102 | Call to pbkdf2 | Rfc2898DeriveBytes.Pbkdf2 uses iteration count of 1000, which is below the minimum of 100000. | +| WeakKDFConfiguration.ps1:44:8:44:102 | Call to pbkdf2 | Rfc2898DeriveBytes.Pbkdf2 uses weak hash algorithm SHA1. Use SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:47:8:47:84 | Call to new | Rfc2898DeriveBytes uses iteration count of 500, which is below the minimum of 100000. | +| WeakKDFConfiguration.ps1:47:8:47:84 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. | +| WeakKDFConfiguration.ps1:50:8:50:140 | Call to new | Rfc2898DeriveBytes uses iteration count of 500, which is below the minimum of 100000. | +| WeakKDFConfiguration.ps1:50:8:50:140 | Call to new | Rfc2898DeriveBytes uses weak hash algorithm SHA1. Use SHA-256 or stronger. | diff --git a/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.ps1 b/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.ps1 new file mode 100644 index 000000000000..956ea66e4924 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.ps1 @@ -0,0 +1,71 @@ +# =================================================================== +# ========== TRUE POSITIVES (should trigger alert) ================== +# =================================================================== + +# --- Case 1: Default iteration count AND default algorithm --- +# BAD: Only password and salt, defaults to 1000 iterations and SHA1 +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt) # $ Alert $ Alert + +# --- Case 2: Low iteration count AND default algorithm --- +# BAD: 1000 iterations is too low, and no algorithm specified +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 1000) # $ Alert $ Alert + +# BAD: 10000 iterations is still below 100k, and no algorithm specified +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 10000) # $ Alert $ Alert + +# BAD: 50000 iterations is still below 100k, and no algorithm specified +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 50000) # $ Alert $ Alert + +# --- Case 3: Default hash algorithm only (iterations are adequate) --- +# BAD: Has iterations but no algorithm, defaults to SHA1 +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000) # $ Alert + +# --- Case 4: Weak hash algorithm explicitly specified --- +# BAD: SHA1 explicitly used +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA1) # $ Alert + +# BAD: MD5 explicitly used +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::MD5) # $ Alert + +# --- Case 5: New-Object pattern (defaults to both issues) --- +# BAD: New-Object with low iteration count (detected as default iterations + default algorithm) +$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($password, $salt, 5000) # $ Alert $ Alert + +# --- Case 6: New-Object pattern with no extra args --- +# BAD: New-Object with default iterations and algorithm +$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($password, $salt) # $ Alert $ Alert + +# --- Case 7: Pbkdf2 static method with low iterations --- +# BAD: Static Pbkdf2 method with 1000 iterations +$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 1000, "SHA256", 32) # $ Alert + +# --- Case 8: Pbkdf2 static method with weak hash --- +# BAD: Static Pbkdf2 method with SHA1 +$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 100000, "SHA1", 32) # $ Alert + +# --- Case 9: Combined issues - low iterations AND no algorithm --- +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 500) # $ Alert $ Alert + +# --- Case 10: Combined issues - low iterations AND weak algorithm --- +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 500, [System.Security.Cryptography.HashAlgorithmName]::SHA1) # $ Alert $ Alert + +# =================================================================== +# ========== TRUE NEGATIVES (should NOT trigger alert) ============== +# =================================================================== + +# --- Safe: Proper iterations and strong hash --- +# GOOD: 100000 iterations with SHA256 +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256) + +# GOOD: 200000 iterations with SHA512 +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 200000, [System.Security.Cryptography.HashAlgorithmName]::SHA512) + +# GOOD: 600000 iterations with SHA256 +$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 600000, [System.Security.Cryptography.HashAlgorithmName]::SHA256) + +# --- Safe: Pbkdf2 with proper config --- +# GOOD: Static Pbkdf2 with adequate iterations and strong hash +$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 100000, "SHA256", 32) + +# GOOD: Static Pbkdf2 with high iterations +$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 600000, "SHA512", 32) diff --git a/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.qlref b/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.qlref new file mode 100644 index 000000000000..05d364481bf4 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-327/WeakKDFConfiguration/WeakKDFConfiguration.qlref @@ -0,0 +1,2 @@ +query: queries/security/cwe-327/WeakKDFConfiguration.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/powershell/ql/test/query-tests/security/cwe-502/UnsafeDeserialization.expected b/powershell/ql/test/query-tests/security/cwe-502/UnsafeDeserialization.expected index 81aa06cc5d11..6eed3468d308 100644 --- a/powershell/ql/test/query-tests/security/cwe-502/UnsafeDeserialization.expected +++ b/powershell/ql/test/query-tests/security/cwe-502/UnsafeDeserialization.expected @@ -16,8 +16,11 @@ | test.ps1:76:49:76:57 | stream15 | test.ps1:74:12:74:42 | Call to read-host | test.ps1:76:49:76:57 | stream15 | This unsafe deserializer deserializes on a $@. | test.ps1:74:12:74:42 | Call to read-host | read from stdin | | test.ps1:81:40:81:47 | input16 | test.ps1:79:12:79:33 | Call to read-host | test.ps1:81:40:81:47 | input16 | This unsafe deserializer deserializes on a $@. | test.ps1:79:12:79:33 | Call to read-host | read from stdin | | test.ps1:86:48:86:55 | bytes17 | test.ps1:84:12:84:40 | Call to read-host | test.ps1:86:48:86:55 | bytes17 | This unsafe deserializer deserializes on a $@. | test.ps1:84:12:84:40 | Call to read-host | read from stdin | -| test.ps1:98:39:98:49 | fileStream | test.ps1:96:14:96:58 | Call to readallbytes | test.ps1:98:39:98:49 | fileStream | This unsafe deserializer deserializes on a $@. | test.ps1:96:14:96:58 | Call to readallbytes | file stream | -| test.ps1:103:41:103:53 | remoteStream | test.ps1:101:16:101:92 | Call to downloaddata | test.ps1:103:41:103:53 | remoteStream | This unsafe deserializer deserializes on a $@. | test.ps1:101:16:101:92 | Call to downloaddata | remote flow source | +| test.ps1:90:30:90:37 | input18 | test.ps1:89:12:89:38 | Call to read-host | test.ps1:90:30:90:37 | input18 | This unsafe deserializer deserializes on a $@. | test.ps1:89:12:89:38 | Call to read-host | read from stdin | +| test.ps1:94:37:94:44 | input19 | test.ps1:93:12:93:41 | Call to read-host | test.ps1:94:37:94:44 | input19 | This unsafe deserializer deserializes on a $@. | test.ps1:93:12:93:41 | Call to read-host | read from stdin | +| test.ps1:98:10:98:17 | input20 | test.ps1:97:12:97:42 | Call to read-host | test.ps1:98:10:98:17 | input20 | This unsafe deserializer deserializes on a $@. | test.ps1:97:12:97:42 | Call to read-host | read from stdin | +| test.ps1:113:39:113:49 | fileStream | test.ps1:111:14:111:58 | Call to readallbytes | test.ps1:113:39:113:49 | fileStream | This unsafe deserializer deserializes on a $@. | test.ps1:111:14:111:58 | Call to readallbytes | file stream | +| test.ps1:118:41:118:53 | remoteStream | test.ps1:116:16:116:92 | Call to downloaddata | test.ps1:118:41:118:53 | remoteStream | This unsafe deserializer deserializes on a $@. | test.ps1:116:16:116:92 | Call to downloaddata | remote flow source | edges | test.ps1:2:1:2:16 | untrustedBase64 | test.ps1:4:69:4:84 | untrustedBase64 | provenance | | | test.ps1:2:20:2:47 | Call to read-host | test.ps1:2:1:2:16 | untrustedBase64 | provenance | Src:MaD:0 | @@ -87,16 +90,22 @@ edges | test.ps1:85:1:85:8 | bytes17 | test.ps1:86:48:86:55 | bytes17 | provenance | | | test.ps1:85:12:85:48 | Call to frombase64string | test.ps1:85:1:85:8 | bytes17 | provenance | | | test.ps1:85:40:85:47 | input17 | test.ps1:85:12:85:48 | Call to frombase64string | provenance | MaD:142 | -| test.ps1:96:1:96:10 | fileBytes | test.ps1:97:45:97:54 | fileBytes | provenance | | -| test.ps1:96:14:96:58 | Call to readallbytes | test.ps1:96:1:96:10 | fileBytes | provenance | Src:MaD:83 | -| test.ps1:97:1:97:11 | fileStream | test.ps1:98:39:98:49 | fileStream | provenance | | -| test.ps1:97:15:97:55 | Call to new | test.ps1:97:1:97:11 | fileStream | provenance | | -| test.ps1:97:45:97:54 | fileBytes | test.ps1:97:15:97:55 | Call to new | provenance | MaD:104 | -| test.ps1:101:1:101:12 | remoteBytes | test.ps1:102:47:102:58 | remoteBytes | provenance | | -| test.ps1:101:16:101:92 | Call to downloaddata | test.ps1:101:1:101:12 | remoteBytes | provenance | Src:MaD:125 | -| test.ps1:102:1:102:13 | remoteStream | test.ps1:103:41:103:53 | remoteStream | provenance | | -| test.ps1:102:17:102:59 | Call to new | test.ps1:102:1:102:13 | remoteStream | provenance | | -| test.ps1:102:47:102:58 | remoteBytes | test.ps1:102:17:102:59 | Call to new | provenance | MaD:104 | +| test.ps1:89:1:89:8 | input18 | test.ps1:90:30:90:37 | input18 | provenance | | +| test.ps1:89:12:89:38 | Call to read-host | test.ps1:89:1:89:8 | input18 | provenance | Src:MaD:0 | +| test.ps1:93:1:93:8 | input19 | test.ps1:94:37:94:44 | input19 | provenance | | +| test.ps1:93:12:93:41 | Call to read-host | test.ps1:93:1:93:8 | input19 | provenance | Src:MaD:0 | +| test.ps1:97:1:97:8 | input20 | test.ps1:98:10:98:17 | input20 | provenance | | +| test.ps1:97:12:97:42 | Call to read-host | test.ps1:97:1:97:8 | input20 | provenance | Src:MaD:0 | +| test.ps1:111:1:111:10 | fileBytes | test.ps1:112:45:112:54 | fileBytes | provenance | | +| test.ps1:111:14:111:58 | Call to readallbytes | test.ps1:111:1:111:10 | fileBytes | provenance | Src:MaD:83 | +| test.ps1:112:1:112:11 | fileStream | test.ps1:113:39:113:49 | fileStream | provenance | | +| test.ps1:112:15:112:55 | Call to new | test.ps1:112:1:112:11 | fileStream | provenance | | +| test.ps1:112:45:112:54 | fileBytes | test.ps1:112:15:112:55 | Call to new | provenance | MaD:104 | +| test.ps1:116:1:116:12 | remoteBytes | test.ps1:117:47:117:58 | remoteBytes | provenance | | +| test.ps1:116:16:116:92 | Call to downloaddata | test.ps1:116:1:116:12 | remoteBytes | provenance | Src:MaD:125 | +| test.ps1:117:1:117:13 | remoteStream | test.ps1:118:41:118:53 | remoteStream | provenance | | +| test.ps1:117:17:117:59 | Call to new | test.ps1:117:1:117:13 | remoteStream | provenance | | +| test.ps1:117:47:117:58 | remoteBytes | test.ps1:117:17:117:59 | Call to new | provenance | MaD:104 | nodes | test.ps1:2:1:2:16 | untrustedBase64 | semmle.label | untrustedBase64 | | test.ps1:2:20:2:47 | Call to read-host | semmle.label | Call to read-host | @@ -183,16 +192,25 @@ nodes | test.ps1:85:12:85:48 | Call to frombase64string | semmle.label | Call to frombase64string | | test.ps1:85:40:85:47 | input17 | semmle.label | input17 | | test.ps1:86:48:86:55 | bytes17 | semmle.label | bytes17 | -| test.ps1:96:1:96:10 | fileBytes | semmle.label | fileBytes | -| test.ps1:96:14:96:58 | Call to readallbytes | semmle.label | Call to readallbytes | -| test.ps1:97:1:97:11 | fileStream | semmle.label | fileStream | -| test.ps1:97:15:97:55 | Call to new | semmle.label | Call to new | -| test.ps1:97:45:97:54 | fileBytes | semmle.label | fileBytes | -| test.ps1:98:39:98:49 | fileStream | semmle.label | fileStream | -| test.ps1:101:1:101:12 | remoteBytes | semmle.label | remoteBytes | -| test.ps1:101:16:101:92 | Call to downloaddata | semmle.label | Call to downloaddata | -| test.ps1:102:1:102:13 | remoteStream | semmle.label | remoteStream | -| test.ps1:102:17:102:59 | Call to new | semmle.label | Call to new | -| test.ps1:102:47:102:58 | remoteBytes | semmle.label | remoteBytes | -| test.ps1:103:41:103:53 | remoteStream | semmle.label | remoteStream | +| test.ps1:89:1:89:8 | input18 | semmle.label | input18 | +| test.ps1:89:12:89:38 | Call to read-host | semmle.label | Call to read-host | +| test.ps1:90:30:90:37 | input18 | semmle.label | input18 | +| test.ps1:93:1:93:8 | input19 | semmle.label | input19 | +| test.ps1:93:12:93:41 | Call to read-host | semmle.label | Call to read-host | +| test.ps1:94:37:94:44 | input19 | semmle.label | input19 | +| test.ps1:97:1:97:8 | input20 | semmle.label | input20 | +| test.ps1:97:12:97:42 | Call to read-host | semmle.label | Call to read-host | +| test.ps1:98:10:98:17 | input20 | semmle.label | input20 | +| test.ps1:111:1:111:10 | fileBytes | semmle.label | fileBytes | +| test.ps1:111:14:111:58 | Call to readallbytes | semmle.label | Call to readallbytes | +| test.ps1:112:1:112:11 | fileStream | semmle.label | fileStream | +| test.ps1:112:15:112:55 | Call to new | semmle.label | Call to new | +| test.ps1:112:45:112:54 | fileBytes | semmle.label | fileBytes | +| test.ps1:113:39:113:49 | fileStream | semmle.label | fileStream | +| test.ps1:116:1:116:12 | remoteBytes | semmle.label | remoteBytes | +| test.ps1:116:16:116:92 | Call to downloaddata | semmle.label | Call to downloaddata | +| test.ps1:117:1:117:13 | remoteStream | semmle.label | remoteStream | +| test.ps1:117:17:117:59 | Call to new | semmle.label | Call to new | +| test.ps1:117:47:117:58 | remoteBytes | semmle.label | remoteBytes | +| test.ps1:118:41:118:53 | remoteStream | semmle.label | remoteStream | subpaths diff --git a/powershell/ql/test/query-tests/security/cwe-502/test.ps1 b/powershell/ql/test/query-tests/security/cwe-502/test.ps1 index 7428591e2bcf..ce386bd07e9d 100644 --- a/powershell/ql/test/query-tests/security/cwe-502/test.ps1 +++ b/powershell/ql/test/query-tests/security/cwe-502/test.ps1 @@ -85,19 +85,34 @@ $input17 = Read-Host "Enter packed data" # $ Source $bytes17 = [Convert]::FromBase64String($input17) [MemoryPack.MemoryPackSerializer]::Deserialize($bytes17) # $ Alert -# Test 18: self-serialized BinaryFormatter data should not be reported by the taint-based query +# Test 18: Import-Clixml with untrusted path +$input18 = Read-Host "Enter file path" # $ Source +$obj18 = Import-Clixml -Path $input18 # $ Alert + +# Test 19: Import-Clixml with untrusted LiteralPath +$input19 = Read-Host "Enter literal path" # $ Source +$obj19 = Import-Clixml -LiteralPath $input19 # $ Alert + +# Test 20: ConvertFrom-CliXml with untrusted input (pipeline) +$input20 = Read-Host "Enter CLIXML string" # $ Source +$obj20 = $input20 | ConvertFrom-CliXml # $ Alert + +# Test 21: Import-Clixml with safe (hardcoded) path should not be flagged +$safeObj = Import-Clixml -Path "C:\safe\config.xml" + +# Test 22: self-serialized BinaryFormatter data should not be reported by the taint-based query $safeFormatter = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter $safeStream = [System.IO.MemoryStream]::new() $safeFormatter.Serialize($safeStream, [PSCustomObject]@{ Name = "local" }) $safeStream.Position = 0 $safeObj = $safeFormatter.Deserialize($safeStream) -# Test 19: file-controlled bytes deserialized with BinaryFormatter +# Test 23: file-controlled bytes deserialized with BinaryFormatter $fileBytes = [System.IO.File]::ReadAllBytes("payload.bin") # $ Source $fileStream = [System.IO.MemoryStream]::new($fileBytes) $fileObj = $safeFormatter.Deserialize($fileStream) # $ Alert -# Test 20: remote-controlled bytes deserialized with BinaryFormatter +# Test 24: remote-controlled bytes deserialized with BinaryFormatter $remoteBytes = [System.Net.WebClient]::new().DownloadData("https://example.com/payload.bin") # $ Source $remoteStream = [System.IO.MemoryStream]::new($remoteBytes) $remoteObj = $safeFormatter.Deserialize($remoteStream) # $ Alert