+
+
+
+ Downloading an artifact (such as an executable, installer, archive, or script) and then
+ using it without verifying its integrity allows an attacker who can tamper with the artifact
+ to execute arbitrary code on the machine. Even when the artifact is retrieved from a trusted
+ source such as GitHub over HTTPS, the contents can still be replaced through a compromised
+ account, a malicious release asset, a poisoned cache or mirror, or a man-in-the-middle attack.
+
+
+ Without an integrity check, there is no guarantee that the bytes that were downloaded are the
+ bytes that were intended, so the downloaded artifact must not be trusted.
+
+
+
+
+
+ Verify the integrity of every downloaded artifact before using it. Compute a cryptographic hash
+ of the downloaded file with Get-FileHash and compare it against a known-good hash
+ that is obtained through a trusted, out-of-band channel (for example, the signed release notes
+ or a published checksum file). Only use the artifact when the computed hash matches the expected
+ value; otherwise, discard it and fail. Where available, prefer verifying a digital signature of
+ the artifact in addition to, or instead of, a plain hash comparison.
+
+
+
+
+
+ In the following example, an executable is downloaded from GitHub and then run directly. Because
+ the artifact is never verified, a tampered download is executed without detection.
+
+
+
+
+ In the following example, the downloaded artifact's SHA-256 hash is compared against the expected
+ value before it is used. The artifact is removed and an error is raised if the integrity check
+ fails, so a tampered download is never executed.
+
+
+
+
+
+
+ Common Weakness Enumeration:
+ CWE-494: Download of Code Without Integrity Check.
+
+
+ Common Weakness Enumeration:
+ CWE-829: Inclusion of Functionality from Untrusted Control Sphere.
+
+
+ Microsoft Learn:
+ Get-FileHash.
+
+
+
+
diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql
new file mode 100644
index 000000000000..f9c1d98aeab4
--- /dev/null
+++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql
@@ -0,0 +1,145 @@
+/**
+ * @name Unvalidated Artifact Download
+ * @description Download of artifact without integrity check.
+ * @kind problem
+ * @problem.severity warning
+ * @security-severity 7.5
+ * @precision high
+ * @id powershell/download-without-integrity-check
+ * @tags security
+ * external/cwe/cwe-494
+ * external/cwe/cwe-829
+ */
+
+import powershell
+import semmle.code.powershell.dataflow.DataFlow
+import semmle.code.powershell.dataflow.TaintTracking
+
+/** Holds if `s` looks like a URL pointing at a trusted artifact host. */
+bindingset[s]
+predicate isTrustedArtifactHost(string s) {
+ s.matches([
+ "%github%",
+ "%gitlab%",
+ "%bitbucket%",
+ "%sourceforge%",
+ "%powershellgallery%",
+ "%nuget%",
+ "%npmjs%",
+ "%pypi%",
+ "%repo1.maven%",
+ "%repo.maven.apache%",
+ "%blob.core.windows%",
+ "%amazonaws%",
+ "%googleapis%",
+ "%azure%",
+ "%visualstudio%",
+ "%jfrog%",
+ "%artifactory%"
+ ])
+}
+
+/** A data-flow node that is tainted by a string constant looking like an artifact URL. */
+class ArtifactUrl extends DataFlow::Node {
+ ArtifactUrl() {
+ exists(DataFlow::Node source, string s |
+ TaintTracking::localTaint(source, this) and
+ s =
+ [
+ source.asExpr().getValue().asString(),
+ source.asExpr().getExpr().(ExpandableStringExpr).getUnexpandedValue()
+ ].toLowerCase() and
+ isTrustedArtifactHost(s) and
+ // Exclude API metadata endpoints (e.g. api.github.com/.../releases/latest),
+ // which return JSON metadata rather than a downloadable artifact.
+ not s.matches(["%api.github.com%", "%api.bitbucket.org%"])
+ )
+ }
+}
+
+/**
+ * A call that downloads an artifact from a trusted host.
+ *
+ * This covers cmdlets and their aliases (`Invoke-WebRequest`/`iwr`,
+ * `Invoke-RestMethod`/`irm`, `Start-BitsTransfer`), native download tools
+ * (`curl`, `wget`, `azcopy`, `aria2c`) and the .NET `WebClient`/`HttpClient`
+ * download methods. The URL may be passed as a named argument (e.g. `-Uri`,
+ * `-Source`), positionally, or as a method argument.
+ */
+class DownloadCall extends DataFlow::CallNode {
+ ArtifactUrl url;
+
+ DownloadCall() {
+ this.getAName() =
+ [
+ // cmdlets and aliases
+ "Invoke-WebRequest", "iwr", "Invoke-RestMethod", "irm", "Start-BitsTransfer",
+ // native command-line download tools
+ "curl", "curl.exe", "wget", "wget.exe", "azcopy", "azcopy.exe", "aria2c", "aria2c.exe",
+ // .NET WebClient / HttpClient download methods
+ "DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync", "DownloadData",
+ "DownloadDataAsync", "DownloadDataTaskAsync", "DownloadString", "DownloadStringAsync",
+ "GetByteArrayAsync", "GetStreamAsync"
+ ] and
+ url = this.getAnArgument()
+ }
+
+ /**
+ * Gets the argument that names the file the artifact is written to, if any.
+ * Downloads that consume the response inline (e.g. `irm ... | iex`) have no
+ * such argument.
+ */
+ DataFlow::Node getOutFileArg() {
+ result =
+ this.getNamedArgument([
+ "outfile", "destination", "outputfile", "outpath", "literalpath", "path", "o"
+ ])
+ or
+ // WebClient.DownloadFile(url, destinationFile): the destination is the 2nd argument.
+ this.getAName() = ["DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync"] and
+ result = this.getArgument(1)
+ }
+}
+
+/**
+ * A call that verifies the integrity of a file, by computing/comparing a hash
+ * or by checking a signature.
+ */
+class IntegrityCheck extends DataFlow::CallNode {
+ IntegrityCheck() {
+ this.getAName() =
+ [
+ "Get-FileHash", "gfh", // hash a file
+ "certutil", "certutil.exe", // certutil -hashfile