Published 02/07/2023
Securing Nuxt Images
By Asher White
Security and usability are two of the most important factors for a website’s success. Unfortunately, sometimes they’re at cross purposes. Images play a vital role in the majority of websites, but they can be tricky to serve. You’d never want to send someone on a 3-inch phone a picture that’s the same size as the one you’d send to someone on a big monitor. Thanks to the <picture>
element, you can load different images based on screen size, but you still need to resize those images on the server.
Resizing every image manually to every screen size would be a lot of tedious work and probably introduce mistakes. So, the images should be automatically resized—and thanks to Nuxt, you can pre-generate the images that your interface will actually use. Nuxt has their own official plugin that does this, @nuxt/image
, which is great plugin, but it’s new and there are still some issues. For one thing, it is designed to run the image resizing server (IPX) in production—but that opens a huge security vulnerability because accepts a lot of URL parameters and then does a computationally expensive operation (resizing an image). For example, you could send /_ipx/s_100x100,f_avif/image.png
, and then /_ipx/s_101x100,f_avif/image.png
, and so on, with almost infinite combinations. If an attacker tried to do this, depending on the server, they could either slow it down for legitimate users, drastically increase hosting costs or even completely take the server down. So, it does not make sense to have the IPX server running in production. The images that actually get used in the interface need to be pre-generated at build, and then no images are resized while the site is live. @nuxt/image
has some support for pre-generating, but there are a couple outstanding bugs that haven’t been fixed yet.
So, I decided to make an in-house version of @nuxt/image
that fixes those bugs and specifically fits my needs. Because @nuxt/image
is open-source (MIT license), I could copy what I needed to make my own version, building on the hard work they’ve already done. I wrote my own dynamic image component that works similarly to the <picture>
element (or the <NuxtPicture>
component from @nuxt/image
), but that pre-generates the images it will use in the correct sizes and format (including versions for retina/HiDPI screens). I chose to only generate WebPs at different sizes, with one legacy fallback in an <img>
element, as I found that AVIFs, at least the implementation that IPX uses, were actually slightly bigger than WebPs in most cases while taking much longer to resize.
The key part of this component is using the X-Nitro-Prerender
header to send a list of URLs to save while the site is building, before it goes live. That way, I can run the IPX image-resizing server while the site is building, generate all the images, and not run it while the site is live, in production, eliminating the denial-of-service vulnerability. The part of DynNuxtPic.vue
that mark the image URLs for pre-generation is shown below:
const webPSrcSet = computed(() => getSrcset("webp"));
const sizesString = computed(
() =>
getSizes(fmttedSrc.value, {
sizes: props.sizes ?? "xs:100vw sm:100vw md:100vw lg:100vw xl:100vw",
}).sizes
);
const fallback = computed(() => getSrcset(legacy.value).src);
if (process.server && process.env.prerender) {
const sources = [
fallback.value,
...(webPSrcSet.value.srcset || "")
.split(/,\s?/)
.map((s) => s.split(" ")[0]),
].filter((s) => s && s.includes("/_ipx/"));
const set = Array.from(new Set(sources));
appendHeader(useRequestEvent(), "X-Nitro-Prerender", set.join(","));
}
The last line shows where I append the X-Nitro-Prerender
header to the request for the page while building, and joins the URLs of all the different sizes and formats used in that image.
Then, I set nitro to prerender the static pages in the nuxt.config.ts
. So, when I build the site with yarn build
, all the images will be built and saved, ready to be deployed to production, and without any security vulnerabilities.