even more defensive programming on image handling

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-22 12:59:46 -06:00
parent 30a0e09a22
commit 607eaae3a9
No known key found for this signature in database
5 changed files with 122 additions and 13 deletions

View file

@ -13,12 +13,12 @@ Work in progress! Currently to do:
- [X] RFC9110 compliant proxy loop detection with defensive programming against known vulnerable proxies - [X] RFC9110 compliant proxy loop detection with defensive programming against known vulnerable proxies
- [X] HTTPs only mode and X-Forwarded-Proto reflection - [X] HTTPs only mode and X-Forwarded-Proto reflection
- [X] Cache-Control header - [X] Cache-Control header
- [X] Rate-limiting on local deployment (untested) - [X] Rate-limiting on local deployment
- [X] Read config from Cloudflare - [X] Read config from Cloudflare
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers) - [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
- [X] Tiered rate-limiting - [X] Tiered rate-limiting
- [ ] Lossy WebP on CF Workers (maybe already works?) - [ ] Lossy WebP on CF Workers (maybe already works?)
- [ ] Cache Results on Cloudflare KV. - [ ] Cache results
- [ ] Handle all possible panics reported by Clippy - [ ] Handle all possible panics reported by Clippy
- [X] Sandboxing the image rendering - [X] Sandboxing the image rendering
- [X] Prometheus-format metrics - [X] Prometheus-format metrics
@ -29,21 +29,29 @@ Work in progress! Currently to do:
Preview at: Preview at:
[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
Local: [https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
Image: Image:
![Syuilo Avatar resived.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) ![Syuilo Avatar resived.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
![Syuilo Avatar resived.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
### SVG rendering ### SVG rendering
(font rendering disabled due to size restrictions) (font rendering disabled due to size restrictions)
[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
Local: [https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
![AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) ![AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
![AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
## Setup and Deployment ## Setup and Deployment
1. Clone this repository. Load the submodules with `git submodule update --init`. 1. Clone this repository. Load the submodules with `git submodule update --init`.

View file

@ -813,6 +813,29 @@ pub struct App<C: UpstreamClient, S: Sandboxing> {
_marker: PhantomData<(C, S)>, _marker: PhantomData<(C, S)>,
} }
#[cfg(feature = "env-local")]
/// Register cancel handler for threads that has ran away for some reason
///
/// This should only happen when some kind of malformed input is happening that causes the image parser to hang
/// without triggering the resource limits.
#[allow(unsafe_code)]
pub fn register_cancel_handler() {
#[cfg(target_family = "unix")]
unsafe {
unsafe fn cancel_handler(_: libc::c_int) {
log::error!("Received cancel signal, stopping thread");
libc::pthread_exit(&sandbox::EXIT_TIMEOUT as *const _ as *mut _);
}
if libc::signal(libc::SIGUSR1, cancel_handler as usize) == libc::SIG_ERR {
log::error!(
"Failed to register cancel handler: {}",
std::io::Error::last_os_error()
);
}
}
}
#[cfg(any(feature = "cf-worker", feature = "reqwest"))] #[cfg(any(feature = "cf-worker", feature = "reqwest"))]
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
impl<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static> App<C, S> { impl<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static> App<C, S> {

View file

@ -225,6 +225,8 @@ fn main() {
log::info!("Spawned Tokio reactor."); log::info!("Spawned Tokio reactor.");
yumechi_no_kuni_proxy_worker::register_cancel_handler();
runtime runtime
.block_on(async move { .block_on(async move {
#[cfg(not(feature = "axum-server"))] #[cfg(not(feature = "axum-server"))]

View file

@ -2,7 +2,8 @@ use std::io::Cursor;
use image::{ use image::{
codecs::{png::PngDecoder, webp::WebPDecoder}, codecs::{png::PngDecoder, webp::WebPDecoder},
AnimationDecoder, DynamicImage, GenericImageView, ImageResult, AnimationDecoder, DynamicImage, GenericImageView, ImageDecoder, ImageReader, ImageResult,
Limits,
}; };
use crate::ImageOptions; use crate::ImageOptions;
@ -34,7 +35,9 @@ pub fn postprocess_webp_image(
data: &[u8], data: &[u8],
opt: &ImageOptions, opt: &ImageOptions,
) -> ImageResult<Option<DynamicImage>> { ) -> ImageResult<Option<DynamicImage>> {
let dec = WebPDecoder::new(Cursor::new(data))?; let mut dec = WebPDecoder::new(Cursor::new(data))?;
dec.set_limits(default_limits())
.expect("Failed to set limits");
if !dec.has_animation() && opt.requested_resize() { if !dec.has_animation() && opt.requested_resize() {
return Ok(Some(postprocess_static_image(data, opt)?)); return Ok(Some(postprocess_static_image(data, opt)?));
@ -56,7 +59,9 @@ pub fn postprocess_webp_image(
/// Postprocesses an PNG image using the given options /// Postprocesses an PNG image using the given options
pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Option<DynamicImage>> { pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Option<DynamicImage>> {
let dec = PngDecoder::new(Cursor::new(data))?; let mut dec = PngDecoder::new(Cursor::new(data))?;
dec.set_limits(default_limits())
.expect("Failed to set limits");
let apng = dec.is_apng()?; let apng = dec.is_apng()?;
@ -81,6 +86,9 @@ pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Opt
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
/// An error that occurred during SVG postprocessing /// An error that occurred during SVG postprocessing
pub enum SvgPostprocessError { pub enum SvgPostprocessError {
/// The complexity of the SVG image exceeded the limit
#[error("SVG complexity limit exceeded: computed complexity is {0}")]
LimitExceeded(usize),
/// An error that occurred during rasterization /// An error that occurred during rasterization
#[error("Rasterization error: {0}")] #[error("Rasterization error: {0}")]
Rasterization(#[from] image::ImageError), Rasterization(#[from] image::ImageError),
@ -123,6 +131,17 @@ pub fn postprocess_svg_image(
}, },
)?; )?;
let complexity = svg.clip_paths().len()
+ svg.filters().len()
+ svg.linear_gradients().len()
+ svg.radial_gradients().len()
+ svg.patterns().len()
+ (svg.size().width() * svg.size().height()) as usize / 1000;
if complexity > 25535 {
return Err(SvgPostprocessError::LimitExceeded(complexity));
}
let size = svg.size(); let size = svg.size();
let clamped = clamp_dimensions((size.width() as u32, size.height() as u32), 800, 800); let clamped = clamp_dimensions((size.width() as u32, size.height() as u32), 800, 800);
@ -144,8 +163,21 @@ pub fn postprocess_svg_image(
)) ))
} }
fn default_limits() -> Limits {
let mut l = Limits::default();
l.max_alloc = Some(64 << 20);
l.max_image_height = Some(8192);
l.max_image_width = Some(8192);
l
}
/// Preprocesses a static image using the given options /// Preprocesses a static image using the given options
pub fn postprocess_static_image(data: &[u8], opt: &ImageOptions) -> ImageResult<DynamicImage> { pub fn postprocess_static_image(data: &[u8], opt: &ImageOptions) -> ImageResult<DynamicImage> {
let mut r = ImageReader::new(Cursor::new(data));
r.limits(default_limits());
Ok(process_static_image_impl( Ok(process_static_image_impl(
image::load_from_memory(data)?, image::load_from_memory(data)?,
opt, opt,

View file

@ -1,7 +1,49 @@
use crate::SandboxConfig; use crate::SandboxConfig;
#[cfg(all(target_family = "unix", feature = "tokio"))] /// Thread termination due to timeout
pub const EXIT_TIMEOUT: usize = 1;
#[cfg(target_os = "linux")]
#[allow(missing_docs)]
pub(crate) mod pthread {
#[link(name = "pthread")]
extern "C" {
pub fn pthread_setcancelstate(state: i32, oldstate: *mut i32) -> i32;
pub fn pthread_setcanceltype(type_: i32, oldtype: *mut i32) -> i32;
}
pub const PTHREAD_CANCEL_ENABLE: i32 = 0;
pub const PTHREAD_CANCEL_ASYNCHRONOUS: i32 = 1;
}
/// Run a function with explicit immediate cancellation
#[allow(unsafe_code)] #[allow(unsafe_code)]
pub(crate) fn pthread_cancelable<F: FnOnce() -> R, R>(f: F) -> R {
unsafe {
let mut oldstate = 0;
let mut oldtype = 0;
#[cfg(target_os = "linux")]
pthread::pthread_setcancelstate(pthread::PTHREAD_CANCEL_ENABLE, &mut oldstate);
#[cfg(target_os = "linux")]
pthread::pthread_setcanceltype(pthread::PTHREAD_CANCEL_ASYNCHRONOUS, &mut oldtype);
log::trace!(
"Running cancelable function, oldstate: {}, oldtype: {}",
oldstate,
oldtype
);
let res = f();
#[cfg(target_os = "linux")]
pthread::pthread_setcancelstate(oldstate, std::ptr::null_mut());
#[cfg(target_os = "linux")]
pthread::pthread_setcanceltype(oldtype, std::ptr::null_mut());
res
}
}
#[cfg(all(target_family = "unix", feature = "tokio"))]
#[allow(unsafe_code, clippy::inline_always)]
#[inline(always)]
/// A bloking tokio task with a cancellation /// A bloking tokio task with a cancellation
pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>( pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
sandbox: &impl Sandboxing, sandbox: &impl Sandboxing,
@ -14,18 +56,20 @@ pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
tokio::spawn(async move { tokio::spawn(async move {
if signal.await.is_ok() { if signal.await.is_ok() {
unsafe { unsafe {
libc::pthread_cancel(tid); libc::pthread_kill(tid, libc::SIGUSR1);
} }
} }
}); });
tokio::task::block_in_place(move || { tokio::task::block_in_place(move || {
pthread_cancelable(|| {
let guard = sandbox.setup(key); let guard = sandbox.setup(key);
let res = f(); let res = f();
drop(guard); drop(guard);
res res
}) })
})
} }
/// A trait for setting up a thread sandboxing environment /// A trait for setting up a thread sandboxing environment