0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-20 22:12:38 -05:00

fix(assets): Solidify Node endpoint (#10284)

* fix(assets): Solidify Node endpoint

* chore: changeset
This commit is contained in:
Erika 2024-03-01 10:23:07 +01:00 committed by GitHub
parent df05138ebe
commit 07f89429a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 143 additions and 13 deletions

View file

@ -0,0 +1,7 @@
---
"astro": patch
---
Fixes an issue where in Node SSR, the image endpoint could be used maliciously to reveal unintended information about the underlying system.
Thanks to Google Security Team for reporting this issue.

View file

@ -1,4 +1,7 @@
import os from 'os';
/* eslint-disable no-console */
import os from 'node:os';
import { isAbsolute } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path';
import { readFile } from 'fs/promises';
import mime from 'mime/lite.js';
@ -7,23 +10,44 @@ import { getConfiguredImageService } from '../internal.js';
import { etag } from '../utils/etag.js';
import { isRemoteAllowed } from '../utils/remotePattern.js';
// @ts-expect-error
import { assetsDir, imageConfig } from 'astro:assets';
import { assetsDir, outDir, imageConfig } from 'astro:assets';
function replaceFileSystemReferences(src: string) {
return os.platform().includes('win32') ? src.replace(/^\/@fs\//, '') : src.replace(/^\/@fs/, '');
}
async function loadLocalImage(src: string, url: URL) {
const filePath = import.meta.env.DEV
? removeQueryString(replaceFileSystemReferences(src))
: new URL('.' + src, assetsDir);
const assetsDirPath = fileURLToPath(assetsDir);
let fileUrl;
if (import.meta.env.DEV) {
fileUrl = pathToFileURL(removeQueryString(replaceFileSystemReferences(src)));
} else {
try {
fileUrl = new URL('.' + src, outDir);
const filePath = fileURLToPath(fileUrl);
if (!isAbsolute(filePath) || !filePath.startsWith(assetsDirPath)) {
return undefined;
}
} catch (err: unknown) {
return undefined;
}
}
let buffer: Buffer | undefined = undefined;
try {
buffer = await readFile(filePath);
buffer = await readFile(fileUrl);
} catch (e) {
const sourceUrl = new URL(src, url.origin);
buffer = await loadRemoteImage(sourceUrl);
// Fallback to try to load the file using `fetch`
try {
const sourceUrl = new URL(src, url.origin);
buffer = await loadRemoteImage(sourceUrl);
} catch (err: unknown) {
console.error('Could not process image request:', err);
return undefined;
}
}
return buffer;
@ -58,7 +82,11 @@ export const GET: APIRoute = async ({ request }) => {
const transform = await imageService.parseURL(url, imageConfig);
if (!transform?.src) {
throw new Error('Incorrect transform returned by `parseURL`');
const err = new Error(
'Incorrect transform returned by `parseURL`. Expected a transform with a `src` property.'
);
console.error('Could not parse image transform from URL:', err);
return new Response('Internal Server Error', { status: 500 });
}
let inputBuffer: Buffer | undefined = undefined;
@ -74,7 +102,7 @@ export const GET: APIRoute = async ({ request }) => {
}
if (!inputBuffer) {
return new Response('Not Found', { status: 404 });
return new Response('Internal Server Error', { status: 500 });
}
const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
@ -89,8 +117,12 @@ export const GET: APIRoute = async ({ request }) => {
},
});
} catch (err: unknown) {
// eslint-disable-next-line no-console
console.error('Could not process image request:', err);
return new Response(`Server Error: ${err}`, { status: 500 });
return new Response(
import.meta.env.DEV ? `Could not process image request: ${err}` : `Internal Server Error`,
{
status: 500,
}
);
}
};

View file

@ -66,13 +66,14 @@ export default function assets({
export { default as Picture } from "astro/components/Picture.astro";
export const imageConfig = ${JSON.stringify(settings.config.image)};
export const assetsDir = new URL(${JSON.stringify(
export const outDir = new URL(${JSON.stringify(
new URL(
isServerLikeOutput(settings.config)
? settings.config.build.client
: settings.config.outDir
)
)});
export const assetsDir = new URL(${JSON.stringify(settings.config.build.assets)}, outDir);
export const getImage = async (options) => await getImageInternal(options, imageConfig);
`;
}

View file

@ -1103,6 +1103,96 @@ describe('astro:image', () => {
assert.equal(response.status, 200);
});
it('endpoint handle malformed requests', async () => {
const badPaths = [
'../../../../../../../../../../../../etc/hosts%00',
'../../../../../../../../../../../../etc/hosts',
'../../boot.ini',
'/../../../../../../../../%2A',
'../../../../../../../../../../../../etc/passwd%00',
'../../../../../../../../../../../../etc/passwd',
'../../../../../../../../../../../../etc/shadow%00',
'../../../../../../../../../../../../etc/shadow',
'/../../../../../../../../../../etc/passwd^^',
'/../../../../../../../../../../etc/shadow^^',
'/../../../../../../../../../../etc/passwd',
'/../../../../../../../../../../etc/shadow',
'/./././././././././././etc/passwd',
'/./././././././././././etc/shadow',
'....................etcpasswd',
'....................etcshadow',
'....................etcpasswd',
'....................etcshadow',
'/..../..../..../..../..../..../etc/passwd',
'/..../..../..../..../..../..../etc/shadow',
'.\\./.\\./.\\./.\\./.\\./.\\./etc/passwd',
'.\\./.\\./.\\./.\\./.\\./.\\./etc/shadow',
'....................etcpasswd%00',
'....................etcshadow%00',
'....................etcpasswd%00',
'....................etcshadow%00',
'%0a/bin/cat%20/etc/passwd',
'%0a/bin/cat%20/etc/shadow',
'%00/etc/passwd%00',
'%00/etc/shadow%00',
'%00../../../../../../etc/passwd',
'%00../../../../../../etc/shadow',
'/../../../../../../../../../../../etc/passwd%00.jpg',
'/../../../../../../../../../../../etc/passwd%00.html',
'/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/passwd',
'/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/shadow',
'/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd,',
'/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/shadow,',
'%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..%25%5c..%25%5c..%00',
'/%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..,%25%5c..%25%5c..%25%5c..%25%5c..%00',
'%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..% 25%5c..%25%5c..%00',
'%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..% 25%5c..%25%5c..%255cboot.ini',
'/%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..,%25%5c..%25%5c..%25%5c..%25%5c..winnt/desktop.ini',
'\\'/bin/cat%20/etc/passwd\\'',
'\\'/bin/cat%20/etc/shadow\\'',
'../../../../../../../../conf/server.xml',
'/../../../../../../../../bin/id|',
'C:/inetpub/wwwroot/global.asa',
'C:inetpubwwwrootglobal.asa',
'C:/boot.ini',
'C:\boot.ini',
'../../../../../../../../../../../../localstart.asp%00',
'../../../../../../../../../../../../localstart.asp',
'../../../../../../../../../../../../boot.ini%00',
'../../../../../../../../../../../../boot.ini',
'/./././././././././././boot.ini',
'/../../../../../../../../../../../boot.ini%00',
'/../../../../../../../../../../../boot.ini',
'/..../..../..../..../..../..../boot.ini',
'/.\\./.\\./.\\./.\\./.\\./.\\./boot.ini',
'....................\boot.ini',
'....................\boot.ini%00',
'....................\boot.ini',
'/../../../../../../../../../../../boot.ini%00.html',
'/../../../../../../../../../../../boot.ini%00.jpg',
'/.../.../.../.../.../ ',
'..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../boot.ini',
'/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/boot.ini',
'../prerender/index.html',
];
const app = await fixture.loadTestAdapterApp();
for (const path of badPaths) {
let request = new Request('http://example.com/_image?href=' + path);
let response = await app.render(request);
const body = await response.text();
assert.equal(response.status, 500);
assert.equal(body.includes('Internal Server Error'), true);
}
// Server should still be running
let request = new Request('http://example.com/');
let response = await app.render(request);
assert.equal(response.status, 200);
});
it('prerendered routes images are built', async () => {
const html = await fixture.readFile('/client/prerender/index.html');
const $ = cheerio.load(html);