Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support blocking untrusted operations #24

Merged
merged 17 commits into from
Sep 4, 2022
16 changes: 16 additions & 0 deletions build/preamble_vips.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ declare module Vips {
*/
function concurrency(concurrency?: number): void | number;

/**
* Block operations that should not be used on untrusted input.
* This blocks many ForeignLoad operations, including vipsload.
atjn marked this conversation as resolved.
Show resolved Hide resolved
* @param state Set to true to block the operations, set to false to reenable them.
atjn marked this conversation as resolved.
Show resolved Hide resolved
*/
function blockUntrusted(state: boolean): void;

/**
* Block a specific operation from running.
* For example, you can disable all foreign loaders with ('VipsForeignLoad', true)
* and then only reenable the png loader with ('VipsForeignLoadPng', false).
* @param name The name of the operation or class of operations.
* @param state Set to true to block the operation, set to false to reenable it.
*/
atjn marked this conversation as resolved.
Show resolved Hide resolved
function operationBlock(name: string, state: boolean): void;

/**
* Call this to shutdown libvips and the runtime of Emscripten.
* This is only needed on Node.js, as the thread pool of
Expand Down
16 changes: 16 additions & 0 deletions lib/vips.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/bindings/init.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

namespace vips {

void block_untrusted_set(bool state) {
vips_block_untrusted_set(state ? 1 : 0);
}

void operation_block_set(const std::string &name, bool state) {
vips_operation_block_set(name.c_str(), state ? 1 : 0);
}

} // namespace vips
3 changes: 3 additions & 0 deletions src/vips-emscripten.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "bindings/connection.h"
#include "bindings/image.h"
#include "bindings/init.h"
#include "bindings/interpolate.h"
#include "bindings/object.h"
#include "bindings/utils.h"
Expand Down Expand Up @@ -433,6 +434,8 @@ EMSCRIPTEN_BINDINGS(my_module) {
function("config", optional_override([]() {
return vips::replace_all(VIPS_CONFIG, ", ", "\n");
}));
function("blockUntrusted", &vips::block_untrusted_set);
function("operationBlock", &vips::operation_block_set);
atjn marked this conversation as resolved.
Show resolved Hide resolved

// Helper for Node.js to shutdown libvips and the runtime of Emscripten
function("shutdown", &shutdown_js);
Expand Down
14 changes: 14 additions & 0 deletions test/unit/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export const jpegFile = getPath('sample.jpg');
export const truncatedFile = getPath('truncated.jpg');
export const pngFile = getPath('sample.png');
export const vipsFile = getPath('sample.vips');
export const tifFile = getPath('sample.tif');
export const tif1File = getPath('1bit.tif');
export const tif2File = getPath('2bit.tif');
Expand All @@ -26,6 +27,7 @@ export const testFiles = [
jpegFile,
truncatedFile,
pngFile,
vipsFile,
tifFile,
tif1File,
tif2File,
Expand Down Expand Up @@ -225,3 +227,15 @@ export function imageToString (im) {
export function makeRepeated (arr, repeats) {
return [].concat(...Array.from({ length: repeats }, () => arr));
}

// allows temporary disabling of cache, which can be enabled again
// with enableCache()
export function disableCache () {
globalThis.cachePreviousMax = vips.Cache.max();
vips.Cache.max(0);
}

// enables the cache again after it has been disabled with disableCache()
export function enableCache () {
vips.Cache.max(globalThis.cachePreviousMax);
}
atjn marked this conversation as resolved.
Show resolved Hide resolved
Binary file added test/unit/images/sample.vips
Binary file not shown.
60 changes: 60 additions & 0 deletions test/unit/test_block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

import * as Helpers from './helpers.js';

describe('block', () => {
it('block_untrusted', function () {
atjn marked this conversation as resolved.
Show resolved Hide resolved
// Needs VIPS file load support
if (!Helpers.have('vipsload')) {
return this.skip();
}

// Some operations can be cached, which means they can return a result despite
// being blocked, if they were run previously. Disabling the cache fixes this issue.
Helpers.disableCache();

// vipsload is an untrusted operation, and should be blocked when blockUntrusted is true

vips.blockUntrusted(true);
expect(() => vips.Image.vipsload(Helpers.vipsFile)).to.throw(/operation is blocked/);

vips.blockUntrusted(false);
expect(() => vips.Image.vipsload(Helpers.vipsFile)).to.not.throw();

vips.blockUntrusted(true);
expect(() => vips.Image.vipsload(Helpers.vipsFile)).to.throw(/operation is blocked/);

// make sure no operations are blocked when the rest of the tests run
vips.blockUntrusted(false);
kleisauke marked this conversation as resolved.
Show resolved Hide resolved
Helpers.enableCache();
});

it('operation_block', function () {
atjn marked this conversation as resolved.
Show resolved Hide resolved
// Needs JPEG and PNG file load support
if (!Helpers.have('jpegload') || !Helpers.have('pngload')) {
return this.skip();
}

// Some operations can be cached, which means they can return a result despite
// being blocked, if they were run previously. Disabling the cache fixes this issue.
Helpers.disableCache();

vips.operationBlock('VipsForeignLoadJpeg', true);
expect(() => vips.Image.jpegload(Helpers.jpegFile)).to.throw(/operation is blocked/);

vips.operationBlock('VipsForeignLoad', false);
vips.operationBlock('VipsForeignLoadPng', true);
expect(() => vips.Image.jpegload(Helpers.jpegFile)).to.not.throw();
expect(() => vips.Image.pngload(Helpers.pngFile)).to.throw(/operation is blocked/);

vips.operationBlock('VipsForeignLoadJpeg', true);
expect(() => vips.Image.jpegload(Helpers.jpegFile)).to.throw(/operation is blocked/);

vips.operationBlock('VipsForeignLoadPng', false);
expect(() => vips.Image.pngload(Helpers.pngFile)).to.not.throw();

// make sure no operations are blocked when the rest of the tests run
vips.operationBlock('VipsForeignLoad', false);
Helpers.enableCache();
});
});