From 1c91bb6be00b4c70eb4f4f61db9d41c5b385cbe0 Mon Sep 17 00:00:00 2001 From: Andy Volk <100514+downtempo@users.noreply.github.com> Date: Sat, 16 May 2026 22:17:46 -0700 Subject: [PATCH] ADBBinCueImage: reject CUE resources outside the base folder Resolve CUE FILE entries through a shared helper that standardizes paths relative to the CUE file and rejects entries that do not remain inside the CUE file's folder. The containment check resolves symlinks on both the resource and the base directory, so a symlink inside the CUE folder cannot redirect a resource outside it. Resolution errors are captured in a strong local and returned to callers outside the per-entry autorelease pool, avoiding a use-after-free on the rejection path. Use the same resolver during bundle import so invalid entries fail before any source file is queued for copying. Manual validation covered a CUE entry using ../outside.bin, a CUE entry using a symlink to an outside file, and a normal nested tracks/inside.bin entry. Assisted-by: OpenAI Codex (gpt-5) --- Boxer/BXDriveBundleImport.m | 11 ++-- Other Sources/ADBToolkit/ADBBinCueImage.h | 5 ++ Other Sources/ADBToolkit/ADBBinCueImage.m | 66 ++++++++++++++++++++--- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/Boxer/BXDriveBundleImport.m b/Boxer/BXDriveBundleImport.m index 6fea61805..f41c96539 100644 --- a/Boxer/BXDriveBundleImport.m +++ b/Boxer/BXDriveBundleImport.m @@ -126,15 +126,18 @@ - (void) main if (self.isCancelled) return; //Work out what to do with the related file paths we've parsed from the cue file - NSURL *baseURL = sourceURL.URLByDeletingLastPathComponent; NSMutableDictionary *revisedPaths = [NSMutableDictionary dictionaryWithCapacity: numRelatedPaths]; for (NSString *fromPath in relatedPaths) { - //Rewrite Windows-style paths - NSString *sanitisedFromPath = [fromPath stringByReplacingOccurrencesOfString: @"\\" withString: @"/"]; + NSError *pathError = nil; + NSURL *fromURL = [ADBBinCueImage resourceURLForRawPath: fromPath inCueAtURL: sourceURL error: &pathError]; + if (!fromURL) + { + self.error = pathError; + return; + } - NSURL *fromURL = [baseURL URLByAppendingPathComponent: sanitisedFromPath]; NSString *fromName = fromURL.lastPathComponent; NSURL *toURL = [destinationURL URLByAppendingPathComponent: fromName]; diff --git a/Other Sources/ADBToolkit/ADBBinCueImage.h b/Other Sources/ADBToolkit/ADBBinCueImage.h index ce3fdee0b..e7e6ecb25 100644 --- a/Other Sources/ADBToolkit/ADBBinCueImage.h +++ b/Other Sources/ADBToolkit/ADBBinCueImage.h @@ -38,8 +38,13 @@ NS_ASSUME_NONNULL_BEGIN /// Returns an array of the track files specified in the specified CUE, /// as absolute OS X filesystem URLs resolved relative to the CUE's location. +/// Returns @c nil and populates @c outError if any resource resolves outside the CUE's folder. + (nullable NSArray *) resourceURLsInCueAtURL: (NSURL *)cueURL error: (out NSError **)outError; +/// Returns a track file URL resolved relative to the specified CUE file. +/// Returns @c nil and populates @c outError if the resource resolves outside the CUE's folder. ++ (nullable NSURL *) resourceURLForRawPath: (NSString *)rawPath inCueAtURL: (NSURL *)cueURL error: (out NSError **)outError; + /// Returns the location of the binary image for the specified CUE file, /// as an absolute OS X filesystem URL resolved relative to the CUE's location. /// Returns @c nil if the binary image path could not be determined. diff --git a/Other Sources/ADBToolkit/ADBBinCueImage.m b/Other Sources/ADBToolkit/ADBBinCueImage.m index cfe84da0f..c9144719d 100644 --- a/Other Sources/ADBToolkit/ADBBinCueImage.m +++ b/Other Sources/ADBToolkit/ADBBinCueImage.m @@ -41,6 +41,26 @@ /// This is used as a sanity check by +isCueAtPath: to avoid scanning large files unnecessarily. #define ADBCueMaxFileSize 10240 +static BOOL ADBURLIsInDirectory(NSURL *URL, NSURL *directoryURL) +{ + NSString *path = URL.URLByStandardizingPath.URLByResolvingSymlinksInPath.path; + NSString *directoryPath = directoryURL.URLByStandardizingPath.URLByResolvingSymlinksInPath.path; + + return [path isEqualToString: directoryPath] || [path hasPrefix: [directoryPath stringByAppendingString: @"/"]]; +} + +static NSError *ADBCueResourcePathError(NSURL *cueURL, NSString *rawPath) +{ + NSString *description = [NSString stringWithFormat: @"The CUE file “%@” refers to a resource outside its folder.", cueURL.lastPathComponent]; + NSString *failureReason = [NSString stringWithFormat: @"The resource path “%@” does not resolve inside the CUE file's folder.", rawPath]; + + return [NSError errorWithDomain: NSCocoaErrorDomain + code: NSFileReadInvalidFileNameError + userInfo: @{ NSLocalizedDescriptionKey: description, + NSLocalizedFailureReasonErrorKey: failureReason, + NSURLErrorKey: cueURL }]; +} + @implementation ADBBinCueImage @@ -70,6 +90,28 @@ + (NSArray *) rawPathsInCueContents: (NSString *)cueContents return paths; } ++ (NSURL *) resourceURLForRawPath: (NSString *)rawPath inCueAtURL: (NSURL *)cueURL error: (out NSError **)outError +{ + NSURL *baseURL = cueURL.URLByDeletingLastPathComponent.URLByStandardizingPath; + + //Rewrite Windows-style paths + NSString *normalizedPath = [rawPath stringByReplacingOccurrencesOfString: @"\\" withString: @"/"]; + + //Form an absolute path with all ../ components resolved. + NSURL *resourceURL = [baseURL URLByAppendingPathComponent: normalizedPath].URLByStandardizingPath; + + if (!ADBURLIsInDirectory(resourceURL, baseURL)) + { + if (outError) + { + *outError = ADBCueResourcePathError(cueURL, rawPath); + } + return nil; + } + + return resourceURL; +} + + (NSArray *) resourceURLsInCueAtURL: (NSURL *)cueURL error: (out NSError **)outError { NSString *cueContents = [[NSString alloc] initWithContentsOfURL: cueURL @@ -81,20 +123,28 @@ + (NSArray *) resourceURLsInCueAtURL: (NSURL *)cueURL error: (out NSError **)out NSArray *rawPaths = [self rawPathsInCueContents: cueContents]; - //The URL relative to which we will resolve the paths in the CUE - NSURL *baseURL = cueURL.URLByDeletingLastPathComponent; - NSMutableArray *resolvedURLs = [NSMutableArray arrayWithCapacity: rawPaths.count]; + NSError *resourceError = nil; for (NSString *rawPath in rawPaths) @autoreleasepool { - //Rewrite Windows-style paths - NSString *normalizedPath = [rawPath stringByReplacingOccurrencesOfString: @"\\" withString: @"/"]; - - //Form an absolute path with all ../ components resolved. - NSURL *resourceURL = [baseURL URLByAppendingPathComponent: normalizedPath].URLByStandardizingPath; + NSURL *resourceURL = [self resourceURLForRawPath: rawPath inCueAtURL: cueURL error: &resourceError]; + if (!resourceURL) + { + break; + } [resolvedURLs addObject: resourceURL]; } + + if (resourceError) + { + if (outError) + { + *outError = resourceError; + } + return nil; + } + return resolvedURLs; }