Open tim-salabim opened 4 years ago
The following example currently works, but only with the evil eval
in https://github.com/r-spatial/leafem/blob/master/inst/htmlwidgets/lib/georaster-for-leaflet/georaster-binding.js#L104
If I change this line to vals = evalMath(arith);
all pixel calculations return 0 as both values[1]
and values[0]
have the value 255. This is a remnant of the previous call to evalMath
in evalDomain
here where the latest combination in the for loop evaluates to [255, 255]
. evalMath
is defined here.
There is some scope creep happening somewhere, but I am not sure where and how to avoid it. Maybe @yeedle has an idea?
library(stars)
library(leaflet)
library(leafem)
tif = system.file("tif/L7_ETMs.tif", package = "stars")
(x1 = read_stars(tif))
strs = st_warp(x1, crs = 4326)
fl = tempfile(fileext = ".tif")
write_stars(strs, fl)
pal = grDevices::colorRampPalette(hcl.colors(9, "viridis"))
ndvi = function(x) (x[4] - x[3]) / (x[4] + x[3])
leaflet() %>%
addTiles() %>%
addGeotiff(
file = fl
, group = "stars"
, resolution = 96
, arith = ndvi
, colorOptions = colorOptions(
palette = pal
)
)
To get this to work:
remotes::install_github("r-spatial/leafem")
I've figured out the issue and have a suggested fix (see below), but I want to clear up a couple of things. eval
is "evil" because it evaluates user-supplied strings in a privileged context. For example, if you run eval(userSuppliedString)
on your server, and the user passes ("exec('rm -rf /')"
, all your server files will be deleted. Function
is just as dangerous as it can accomplish the exact same thing. However, In context of this package (which is a user running eval in their own browser which is sandboxed in the first place), any real danger, like a user accidentally or maliciously running harmful code, is honestly minimal to non-existent. But eval
(and Function
) can still be a footgun if the user does pass in some wonky string that then causes the package to behave in unexpected ways. This is actually the type of danger for which Function
is less "dangerous" than eval
- and it is this exact same reason that is causing the issue you are describing.
The difference between eval
and Function
is that eval
has access to local scope which means it can reassign local variables. On the other hand, Function
only has access to global scope, making it impossible to mess up local variables. In evalDomain
, the values
variable is in global scope. The function has access to it and can operate on it. However, when you use it inside the pixelValuesToColorFn
function, you want it to access the local values
variable, but since it's a Function
call and not an eval
call, it still can only access the global values
which is set to the last value it was set to inside the evalDomain
loop.
The solution is to let the evalMath
function accept a values
argument, and then pass in the values whenever you call evalMath
. This would look as follows (btw, I suggest you use something like _values
or something similarly obfuscated so that if other code uses values
as a variable in global scope it shouldn't clash with this code):
function evalMath(a, values) {
return Function('values', 'with(Math) return ' + a)(values);
}
When you call evalMath
pass in both the user's string and the values argument, e.g.;
pixelValuesToColorFn = values => {
let vals;
if (arith === null) {
if (bands.length > 1) {
bands = bands[0];
}
vals = values[bands];
}
if (arith !== null) {
vals = evalMath(arith, values);
}
let clr = scale.domain(domain);
if (isNaN(vals)) return nacol;
return clr(vals).hex();
};
and:
function evalDomain(arr, arith) {
var out = [];
for (let i = 0; i < arr.length; i++) {
values = arr[i];
out.push(evalMath(arith, values));
}
return [Math.min(...out), Math.max(...out)];
}
I hope you find the above helpful!
@yeedle many many thanks! This works like a charm! I've included you as a contributor to the package (if that's ok with you).
For example, if you run eval(userSuppliedString) on your server, and the user passes ("exec('rm -rf /')", all your server files will be deleted. Function is just as dangerous as it can accomplish the exact same thing. However, In context of this package (which is a user running eval in their own browser which is sandboxed in the first place), any real danger, like a user accidentally or maliciously running harmful code, is honestly minimal to non-existent.
Does this mean that if someone is hosting a map that includes eval
or Function
that it can potentially be hijacked by someone to ingest someEvilString
and cause a lot of trouble? Or is this an unlikely scenario?
I think any case in which you execute user-supplied strings opens you up to some vulnerabilities. Personally I believe that in this particular case it's highly unlikely. Even in the case of a hosted map, the user is still running the javascript in their own sandboxed browser. The only exception I can think of is a case where the javascript will get executed in a headless environment on a server, but I think the way you deparse and translate the R function into a JS expression makes even such a scenario very hard to exploit. If you want to completely stay away from any such concern, I'd suggest you use math.js's math expression parser or expr-eval instead.
Thanks again @yeedle for a great explanation! And especially for the pointer to expr-eval. Do you have any experience with it? It looks like it could be very useful here.
Don't have much experience but usage is pretty straightforward. You have your expression as a string, and then you have your variable. You instantiate a parser const parser = new Parser()
, and pass the string to the parser (const expression = passer.parse(stringExpression)
). You can then use the expression instance to evaluate different values with it expression.evaluate(obj)
If you want I can open a PR replacing Function
with expr-eval.
Thanks @yeedle ! A PR would be fantastic
Yeah, the safer the better. Especially as I still quite comprehend the potential risks. So better be safe than sorry I guess.
The examples using a pixelValuesToColorFn = myCustomJSFunc
in #25 dont work for me.
colorOptions
doesnt exist on the jS side or is undefined, so colorOptions.palette
cannot be read.
Am I missing something?
In #25 I've been keeping track of the development for the new js functionality to add geotiff data to a leaflet map. The API has changed enough so that most examples over there won't work anymore. Hence, dev-tracking will continue here.
The "new" API has a few advantages over the old one:
addGeotiff
as opposed toaddGeoraster
arith
argument as opposed to some string representationarith
Math.
functions (https://twitter.com/TimSalabim3/status/1301078413315317761) though we are still using the evileval
in one place... to be addressedaddRGB
(for both stars and raster objects). We setrgb
toTRUE
inaddGeotiff
and the specifiedbands
will be used to create a RGB image (again, this happens in the browser - no pre-calculation in R)Examples to follow...