diff --git a/.changeset/fluffy-timers-remain.md b/.changeset/fluffy-timers-remain.md
new file mode 100644
index 000000000..80d52cb2e
--- /dev/null
+++ b/.changeset/fluffy-timers-remain.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fix `astro:assets` not respecting EXIF rotation
diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts
index c5f9cf935..8b7611db2 100644
--- a/packages/astro/src/assets/services/service.ts
+++ b/packages/astro/src/assets/services/service.ts
@@ -56,7 +56,7 @@ interface SharedServiceProps {
 
 export type ExternalImageService = SharedServiceProps;
 
-type LocalImageTransform = {
+export type LocalImageTransform = {
 	src: string;
 	[key: string]: any;
 };
diff --git a/packages/astro/src/assets/services/sharp.ts b/packages/astro/src/assets/services/sharp.ts
index f77a87654..7f70b9268 100644
--- a/packages/astro/src/assets/services/sharp.ts
+++ b/packages/astro/src/assets/services/sharp.ts
@@ -43,6 +43,9 @@ const sharpService: LocalImageService = {
 
 		let result = sharp(inputBuffer, { failOnError: false, pages: -1 });
 
+		// always call rotate to adjust for EXIF data orientation
+		result.rotate();
+
 		// Never resize using both width and height at the same time, prioritizing width.
 		if (transform.height && !transform.width) {
 			result.resize({ height: transform.height });
diff --git a/packages/astro/src/assets/services/squoosh.ts b/packages/astro/src/assets/services/squoosh.ts
index d9307a219..2ed127ade 100644
--- a/packages/astro/src/assets/services/squoosh.ts
+++ b/packages/astro/src/assets/services/squoosh.ts
@@ -1,11 +1,13 @@
 // TODO: Investigate removing this service once sharp lands WASM support, as libsquoosh is deprecated
 
 import type { ImageOutputFormat, ImageQualityPreset } from '../types.js';
+import { imageMetadata } from '../utils/metadata.js';
 import {
 	baseService,
 	parseQuality,
 	type BaseServiceTransform,
 	type LocalImageService,
+	type LocalImageTransform,
 } from './service.js';
 import { processBuffer } from './vendor/squoosh/image-pool.js';
 import type { Operation } from './vendor/squoosh/image.js';
@@ -28,6 +30,30 @@ const qualityTable: Record<
 	// Squoosh's PNG encoder does not support a quality setting, so we can skip that here
 };
 
+async function getRotationForEXIF(
+	transform: LocalImageTransform,
+	inputBuffer: Buffer
+): Promise<Operation | undefined> {
+	// check EXIF orientation data and rotate the image if needed
+	const meta = await imageMetadata(transform.src, inputBuffer);
+
+	if (!meta) return undefined;
+
+	// EXIF orientations are a bit hard to read, but the numbers are actually standard. See https://exiftool.org/TagNames/EXIF.html for a list.
+	// Various illustrations can also be found online for a more graphic representation, it's a bit old school.
+	switch (meta.orientation) {
+		case 3:
+		case 4:
+			return { type: 'rotate', numRotations: 2 };
+		case 5:
+		case 6:
+			return { type: 'rotate', numRotations: 1 };
+		case 7:
+		case 8:
+			return { type: 'rotate', numRotations: 3 };
+	}
+}
+
 const service: LocalImageService = {
 	validateOptions: baseService.validateOptions,
 	getURL: baseService.getURL,
@@ -43,6 +69,12 @@ const service: LocalImageService = {
 
 		const operations: Operation[] = [];
 
+		const rotation = await getRotationForEXIF(transform, inputBuffer);
+
+		if (rotation) {
+			operations.push(rotation);
+		}
+
 		// Never resize using both width and height at the same time, prioritizing width.
 		if (transform.height && !transform.width) {
 			operations.push({
diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts
index 31a7106ec..d4a8c4c89 100644
--- a/packages/astro/src/assets/types.ts
+++ b/packages/astro/src/assets/types.ts
@@ -24,6 +24,7 @@ export interface ImageMetadata {
 	width: number;
 	height: number;
 	format: ImageInputFormat;
+	orientation?: number;
 }
 
 /**
diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/emitAsset.ts
index b1e01b3e4..e1d66dfc7 100644
--- a/packages/astro/src/assets/utils/emitAsset.ts
+++ b/packages/astro/src/assets/utils/emitAsset.ts
@@ -2,13 +2,14 @@ import fs from 'node:fs';
 import path from 'node:path';
 import { fileURLToPath, pathToFileURL } from 'node:url';
 import { prependForwardSlash, slash } from '../../core/path.js';
-import { imageMetadata, type Metadata } from './metadata.js';
+import type { ImageMetadata } from '../types.js';
+import { imageMetadata } from './metadata.js';
 
 export async function emitESMImage(
 	id: string | undefined,
 	watchMode: boolean,
 	fileEmitter: any
-): Promise<Metadata | undefined> {
+): Promise<ImageMetadata | undefined> {
 	if (!id) {
 		return undefined;
 	}
diff --git a/packages/astro/src/assets/utils/metadata.ts b/packages/astro/src/assets/utils/metadata.ts
index 8d2b5c6a2..8d1f1bc4a 100644
--- a/packages/astro/src/assets/utils/metadata.ts
+++ b/packages/astro/src/assets/utils/metadata.ts
@@ -3,14 +3,10 @@ import { fileURLToPath } from 'node:url';
 import type { ImageInputFormat, ImageMetadata } from '../types.js';
 import imageSize from '../vendor/image-size/index.js';
 
-export interface Metadata extends ImageMetadata {
-	orientation?: number;
-}
-
 export async function imageMetadata(
 	src: URL | string,
 	data?: Buffer
-): Promise<Metadata | undefined> {
+): Promise<ImageMetadata | undefined> {
 	let file = data;
 	if (!file) {
 		try {
