@@ -15,62 +15,126 @@ import powershell
1515import semmle.code.powershell.dataflow.DataFlow
1616import semmle.code.powershell.dataflow.TaintTracking
1717
18- predicate invokeGitHub ( DataFlow:: CallNode call , DataFlow:: Node out ) {
19- exists ( DataFlow:: Node source , string s , DataFlow:: Node uri |
20- call .getAName ( ) = [ "Invoke-WebRequest" , "iwr" , "Invoke-RestMethod" , "irm" , "curl" , "wget" ] and
21- uri = call .getNamedArgument ( "uri" ) and
22- TaintTracking:: localTaint ( source , uri ) and
23- s .toLowerCase ( )
24- .matches ( [
25- "%github%" ,
26- "%gitlab%" ,
27- "%bitbucket%" ,
28- "%sourceforge%" ,
29- "%powershellgallery%" ,
30- "%nuget%" ,
31- "%npmjs%" ,
32- "%pypi%" ,
33- "%repo1.maven%" ,
34- "%repo.maven.apache%" ,
35- "%blob.core.windows%" ,
36- "%amazonaws%" ,
37- "%googleapis%" ,
38- "%azure%" ,
39- "%visualstudio%" ,
40- "%jfrog%" ,
41- "%artifactory%"
42- ] ) and
18+ /** Holds if `s` looks like a URL pointing at a trusted artifact host. */
19+ bindingset [ s]
20+ predicate isTrustedArtifactHost ( string s ) {
21+ s .toLowerCase ( )
22+ .matches ( [
23+ "%github%" ,
24+ "%gitlab%" ,
25+ "%bitbucket%" ,
26+ "%sourceforge%" ,
27+ "%powershellgallery%" ,
28+ "%nuget%" ,
29+ "%npmjs%" ,
30+ "%pypi%" ,
31+ "%repo1.maven%" ,
32+ "%repo.maven.apache%" ,
33+ "%blob.core.windows%" ,
34+ "%amazonaws%" ,
35+ "%googleapis%" ,
36+ "%azure%" ,
37+ "%visualstudio%" ,
38+ "%jfrog%" ,
39+ "%artifactory%"
40+ ] )
41+ }
42+
43+ /** Holds if `node` is tainted by a string constant that looks like an artifact URL. */
44+ predicate isArtifactUrl ( DataFlow:: Node node ) {
45+ exists ( DataFlow:: Node source , string s |
46+ TaintTracking:: localTaint ( source , node ) and
4347 s =
4448 [
4549 source .asExpr ( ) .getValue ( ) .asString ( ) ,
4650 source .asExpr ( ) .getExpr ( ) .( ExpandableStringExpr ) .getUnexpandedValue ( )
4751 ] and
48- out = call . getNamedArgument ( "outfile" )
52+ isTrustedArtifactHost ( s )
4953 )
5054}
5155
56+ /**
57+ * Holds if `call` downloads an artifact, where `url` is the argument that
58+ * carries the (trusted-host) download URL.
59+ *
60+ * This covers cmdlets and their aliases (`Invoke-WebRequest`/`iwr`,
61+ * `Invoke-RestMethod`/`irm`, `Start-BitsTransfer`), native download tools
62+ * (`curl`, `wget`, `azcopy`, `aria2c`) and the .NET `WebClient`/`HttpClient`
63+ * download methods. The URL may be passed as a named argument (e.g. `-Uri`,
64+ * `-Source`), positionally, or as a method argument.
65+ */
66+ predicate downloadCall ( DataFlow:: CallNode call , DataFlow:: Node url ) {
67+ call .getAName ( ) =
68+ [
69+ // cmdlets and aliases
70+ "Invoke-WebRequest" , "iwr" , "Invoke-RestMethod" , "irm" , "Start-BitsTransfer" ,
71+ // native command-line download tools
72+ "curl" , "curl.exe" , "wget" , "wget.exe" , "azcopy" , "azcopy.exe" , "aria2c" , "aria2c.exe" ,
73+ // .NET WebClient / HttpClient download methods
74+ "DownloadFile" , "DownloadFileAsync" , "DownloadFileTaskAsync" , "DownloadData" ,
75+ "DownloadDataAsync" , "DownloadDataTaskAsync" , "DownloadString" , "DownloadStringAsync" ,
76+ "GetByteArrayAsync" , "GetStreamAsync"
77+ ] and
78+ url = call .getAnArgument ( ) and
79+ isArtifactUrl ( url )
80+ }
81+
82+ /**
83+ * Gets the argument of `call` that names the file the artifact is written to,
84+ * if any. Downloads that consume the response inline (e.g. `irm ... | iex`)
85+ * have no such argument.
86+ */
87+ DataFlow:: Node getOutFileArg ( DataFlow:: CallNode call ) {
88+ downloadCall ( call , _) and
89+ (
90+ result =
91+ call .getNamedArgument ( [
92+ "outfile" , "destination" , "outputfile" , "outpath" , "literalpath" , "path" , "o"
93+ ] )
94+ or
95+ // WebClient.DownloadFile(url, destinationFile): the destination is the 2nd argument.
96+ call .getAName ( ) = [ "DownloadFile" , "DownloadFileAsync" , "DownloadFileTaskAsync" ] and
97+ result = call .getArgument ( 1 )
98+ )
99+ }
100+
101+ /**
102+ * Holds if `check` verifies the integrity of the file referred to by `file`,
103+ * by computing/comparing a hash or by checking a signature.
104+ */
105+ predicate integrityCheck ( DataFlow:: CallNode check , DataFlow:: Node file ) {
106+ check .getAName ( ) =
107+ [
108+ "Get-FileHash" , "gfh" , // hash a file
109+ "certutil" , "certutil.exe" , // certutil -hashfile <file> SHA256
110+ "ComputeHash" , // [SHA256]::Create().ComputeHash(...)
111+ "Get-AuthenticodeSignature" , "Test-FileCatalog" , // signature / catalog checks
112+ "cosign" , "cosign.exe" , "gpg" , "gpg.exe" // external signature verification
113+ ] and
114+ file = check .getAnArgument ( )
115+ }
116+
52117module Conf implements DataFlow:: ConfigSig {
53- predicate isSource ( DataFlow:: Node source ) { invokeGitHub ( _ , source ) }
118+ predicate isSource ( DataFlow:: Node source ) { source = getOutFileArg ( _ ) }
54119
55- predicate isSink ( DataFlow:: Node sink ) {
56- exists ( DataFlow:: CallNode hasher |
57- hasher .getAName ( ) = [ "Get-FileHash" , "gfh" ] and
58- sink = hasher .getNamedArgument ( [ "path" , "literalpath" ] )
59- )
60- }
120+ predicate isSink ( DataFlow:: Node sink ) { integrityCheck ( _, sink ) }
61121}
62122
63123module Flow = DataFlow:: Global< Conf > ;
64124
65- predicate isHashed ( DataFlow:: Node out ) {
125+ /** Holds if the downloaded file `out` flows to an integrity check. */
126+ predicate isVerified ( DataFlow:: Node out ) {
66127 exists ( Flow:: PathNode source |
67128 source .getNode ( ) = out and
68129 source .isSource ( )
69130 )
70131}
71132
72- from DataFlow:: CallNode call , DataFlow :: Node out
133+ from DataFlow:: CallNode call
73134where
74- invokeGitHub ( call , out ) and
75- not isHashed ( out )
76- select call , "This downloads an artifact from GitHub without checking its hash."
135+ downloadCall ( call , _) and
136+ // Report unless the file that was downloaded is later verified. A download
137+ // with no output-file argument cannot be hash-verified, so it is reported.
138+ not exists ( DataFlow:: Node out | out = getOutFileArg ( call ) and isVerified ( out ) )
139+ select call ,
140+ "This downloads an artifact without verifying its integrity (e.g. a hash or signature check)."
0 commit comments