Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,13 @@ extensions:
pack: microsoft/powershell-all
extensible: summaryModel
data:
- ["system.io.path!", "Method[getfullpath]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.path!", "Method[getfullpath]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[readallbytes]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[readallbytesasync]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[appendtext]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[createtext]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[readalllines]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[readalllinesasync]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[readalltext]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.file!", "Method[readalltextasync]", "Argument[0]", "ReturnValue", "taint"]
- ["system.io.fileinfo", "Method[createtext]", "Argument[0]", "ReturnValue", "taint"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">

<qhelp>

<overview>
<p>
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.
</p>
<p>
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.
</p>
</overview>

<recommendation>
<p>
Verify the integrity of every downloaded artifact before using it. Compute a cryptographic hash
of the downloaded file with <code>Get-FileHash</code> 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.
</p>
</recommendation>

<example>
<p>
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.
</p>
<sample src="examples/powershell/DownloadWithoutIntegrityCheckBad.ps1" />

<p>
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.
</p>
<sample src="examples/powershell/DownloadWithoutIntegrityCheckGood.ps1" />
</example>

<references>
<li>
Common Weakness Enumeration:
<a href="https://cwe.mitre.org/data/definitions/494.html">CWE-494: Download of Code Without Integrity Check</a>.
</li>
<li>
Common Weakness Enumeration:
<a href="https://cwe.mitre.org/data/definitions/829.html">CWE-829: Inclusion of Functionality from Untrusted Control Sphere</a>.
</li>
<li>
Microsoft Learn:
<a href="https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/get-filehash">Get-FileHash</a>.
</li>
</references>

</qhelp>
Original file line number Diff line number Diff line change
@@ -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 <file> SHA256
"ComputeHash", // [SHA256]::Create().ComputeHash(...)
"Get-AuthenticodeSignature", "Test-FileCatalog", // signature / catalog checks
"cosign", "cosign.exe", "gpg", "gpg.exe" // external signature verification
]
}

/** Gets an argument referring to the file whose integrity is being checked. */
DataFlow::Node getFile() { result = this.getAnArgument() }
}

module Conf implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source = any(DownloadCall c).getOutFileArg() }

predicate isSink(DataFlow::Node sink) { sink = any(IntegrityCheck c).getFile() }
}

module Flow = TaintTracking::Global<Conf>;

/** Holds if the downloaded file `out` flows to an integrity check. */
predicate isVerified(DataFlow::Node out) {
exists(Flow::PathNode source |
source.getNode() = out and
source.isSource()
)
}

from DownloadCall call, DataFlow::Node out
where
out = call.getOutFileArg() and
not isVerified(out)
select call,
"This downloads an artifact without verifying its integrity (e.g. a hash or signature check)."
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# BAD: An artifact is downloaded from GitHub and then executed without
# verifying that its contents match a known-good hash. If the release asset is
# replaced (for example, through a compromised account, a tampered mirror, or a
# man-in-the-middle attack), arbitrary code runs on the machine.
$uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe"
$outFile = Join-Path $env:TEMP "installer.exe"

Invoke-WebRequest -Uri $uri -OutFile $outFile

Start-Process -FilePath $outFile -Wait
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# GOOD: The artifact is downloaded from GitHub and its SHA-256 hash is compared
# against the expected value (published by the author through a trusted channel,
# such as the signed release notes) before it is used. The artifact is only
# executed once its integrity has been confirmed.
$uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe"
$outFile = Join-Path $env:TEMP "installer.exe"
$expectedHash = "8c954cd9b6c8f8180a05f1de66ee555f0c44b4c6738120785d827521bb8d54df"

Invoke-WebRequest -Uri $uri -OutFile $outFile

$actualHash = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash

if ($actualHash -ne $expectedHash) {
Remove-Item -Path $outFile -Force
throw "Integrity check failed: '$outFile' does not match the expected hash."
}

Start-Process -FilePath $outFile -Wait
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
| test.ps1:22:3:22:47 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
| test.ps1:40:3:40:103 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
| test.ps1:45:3:45:89 | Call to iwr | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
| test.ps1:66:3:66:95 | Call to downloadfile | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
| test.ps1:71:3:71:118 | Call to downloadfile | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
| test.ps1:76:3:76:116 | Call to start-bitstransfer | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
| test.ps1:81:3:81:91 | Call to curl | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
Loading
Loading