1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/common/mac/cfbundle_blocker.h" 6 7 #include <CoreFoundation/CoreFoundation.h> 8 #import <Foundation/Foundation.h> 9 10 #include "base/logging.h" 11 #include "base/mac/mac_util.h" 12 #include "base/mac/scoped_cftyperef.h" 13 #include "base/mac/scoped_nsautorelease_pool.h" 14 #import "base/mac/scoped_nsobject.h" 15 #include "base/strings/sys_string_conversions.h" 16 #include "third_party/mach_override/mach_override.h" 17 18 extern "C" { 19 20 // _CFBundleLoadExecutableAndReturnError is the internal implementation that 21 // results in a dylib being loaded via dlopen. Both CFBundleLoadExecutable and 22 // CFBundleLoadExecutableAndReturnError are funneled into this routine. Other 23 // CFBundle functions may also call directly into here, perhaps due to 24 // inlining their calls to CFBundleLoadExecutable. 25 // 26 // See CF-476.19/CFBundle.c (10.5.8), CF-550.43/CFBundle.c (10.6.8), and 27 // CF-635/Bundle.c (10.7.0) and the disassembly of the shipping object code. 28 // 29 // Because this is a private function not declared by 30 // <CoreFoundation/CoreFoundation.h>, provide a declaration here. 31 Boolean _CFBundleLoadExecutableAndReturnError(CFBundleRef bundle, 32 Boolean force_global, 33 CFErrorRef* error); 34 35 } // extern "C" 36 37 namespace chrome { 38 namespace common { 39 namespace mac { 40 41 namespace { 42 43 // Returns an autoreleased array of paths that contain plug-ins that should be 44 // forbidden to load. Each element of the array will be a string containing 45 // an absolute pathname ending in '/'. 46 NSArray* BlockedPaths() { 47 NSMutableArray* blocked_paths; 48 49 { 50 base::mac::ScopedNSAutoreleasePool autorelease_pool; 51 52 // ~/Library, /Library, and /Network/Library. Things in /System/Library 53 // aren't blacklisted. 54 NSArray* blocked_prefixes = 55 NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, 56 NSUserDomainMask | 57 NSLocalDomainMask | 58 NSNetworkDomainMask, 59 YES); 60 61 // Everything in the suffix list has a trailing slash so as to only block 62 // loading things contained in these directories. 63 NSString* const blocked_suffixes[] = { 64 #if !defined(__LP64__) 65 // Contextual menu manager plug-ins are unavailable to 64-bit processes. 66 // http://developer.apple.com/library/mac/releasenotes/Cocoa/AppKitOlderNotes.html#NSMenu 67 // Contextual menu plug-ins are loaded when a contextual menu is opened, 68 // for example, from within 69 // +[NSMenu popUpContextMenu:withEvent:forView:]. 70 @"Contextual Menu Items/", 71 72 // Input managers are deprecated, would only be loaded under specific 73 // circumstances, and are entirely unavailable to 64-bit processes. 74 // http://developer.apple.com/library/mac/releasenotes/Cocoa/AppKitOlderNotes.html#NSInputManager 75 // Input managers are loaded when the NSInputManager class is 76 // initialized. 77 @"InputManagers/", 78 #endif // __LP64__ 79 80 // Don't load third-party scripting additions either. Scripting 81 // additions are loaded by AppleScript from within AEProcessAppleEvent 82 // in response to an Apple Event. 83 @"ScriptingAdditions/" 84 85 // This list is intentionally incomplete. For example, it doesn't block 86 // printer drivers or Internet plug-ins. 87 }; 88 89 NSUInteger blocked_paths_count = [blocked_prefixes count] * 90 arraysize(blocked_suffixes); 91 92 // Not autoreleased here, because the enclosing pool is scoped too 93 // narrowly. 94 blocked_paths = 95 [[NSMutableArray alloc] initWithCapacity:blocked_paths_count]; 96 97 // Build a flat list by adding each suffix to each prefix. 98 for (NSString* blocked_prefix in blocked_prefixes) { 99 for (size_t blocked_suffix_index = 0; 100 blocked_suffix_index < arraysize(blocked_suffixes); 101 ++blocked_suffix_index) { 102 NSString* blocked_suffix = blocked_suffixes[blocked_suffix_index]; 103 NSString* blocked_path = 104 [blocked_prefix stringByAppendingPathComponent:blocked_suffix]; 105 106 [blocked_paths addObject:blocked_path]; 107 } 108 } 109 110 DCHECK_EQ([blocked_paths count], blocked_paths_count); 111 } 112 113 return [blocked_paths autorelease]; 114 } 115 116 // Returns true if bundle_path identifies a path within a blocked directory. 117 // Blocked directories are those returned by BlockedPaths(). 118 bool IsBundlePathBlocked(NSString* bundle_path) { 119 static NSArray* blocked_paths = [BlockedPaths() retain]; 120 121 for (NSString* blocked_path in blocked_paths) { 122 NSUInteger blocked_path_length = [blocked_path length]; 123 124 // Do a case-insensitive comparison because most users will be on 125 // case-insensitive HFS+ filesystems and it's cheaper than asking the 126 // disk. This is like [bundle_path hasPrefix:blocked_path] but is 127 // case-insensitive. 128 if ([bundle_path length] >= blocked_path_length && 129 [bundle_path compare:blocked_path 130 options:NSCaseInsensitiveSearch 131 range:NSMakeRange(0, blocked_path_length)] == 132 NSOrderedSame) { 133 // If bundle_path is inside blocked_path (it has blocked_path as a 134 // prefix), refuse to load it. 135 return true; 136 } 137 } 138 139 // bundle_path is not inside any blocked_path from blocked_paths. 140 return false; 141 } 142 143 typedef Boolean (*_CFBundleLoadExecutableAndReturnError_Type)(CFBundleRef, 144 Boolean, 145 CFErrorRef*); 146 147 // Call this to execute the original implementation of 148 // _CFBundleLoadExecutableAndReturnError. 149 _CFBundleLoadExecutableAndReturnError_Type 150 g_original_underscore_cfbundle_load_executable_and_return_error; 151 152 Boolean ChromeCFBundleLoadExecutableAndReturnError(CFBundleRef bundle, 153 Boolean force_global, 154 CFErrorRef* error) { 155 base::mac::ScopedNSAutoreleasePool autorelease_pool; 156 157 DCHECK(g_original_underscore_cfbundle_load_executable_and_return_error); 158 159 base::ScopedCFTypeRef<CFURLRef> url_cf(CFBundleCopyBundleURL(bundle)); 160 base::scoped_nsobject<NSString> path(base::mac::CFToNSCast( 161 CFURLCopyFileSystemPath(url_cf, kCFURLPOSIXPathStyle))); 162 163 NSString* bundle_id = base::mac::CFToNSCast(CFBundleGetIdentifier(bundle)); 164 165 NSDictionary* bundle_dictionary = 166 base::mac::CFToNSCast(CFBundleGetInfoDictionary(bundle)); 167 NSString* version = [bundle_dictionary objectForKey: 168 base::mac::CFToNSCast(kCFBundleVersionKey)]; 169 if (![version isKindOfClass:[NSString class]]) { 170 // Deal with pranksters. 171 version = nil; 172 } 173 174 if (IsBundlePathBlocked(path) && !IsBundleAllowed(bundle_id, version)) { 175 NSString* bundle_id_print = bundle_id ? bundle_id : @"(nil)"; 176 NSString* version_print = version ? version : @"(nil)"; 177 178 // Provide a hint for the user (or module developer) to figure out 179 // that the bundle was blocked. 180 LOG(INFO) << "Blocking attempt to load bundle " 181 << [bundle_id_print UTF8String] 182 << " version " 183 << [version_print UTF8String] 184 << " at " 185 << [path fileSystemRepresentation]; 186 187 if (error) { 188 base::ScopedCFTypeRef<CFStringRef> app_bundle_id( 189 base::SysUTF8ToCFStringRef(base::mac::BaseBundleID())); 190 191 // 0xb10c10ad = "block load" 192 const CFIndex kBundleLoadBlocked = 0xb10c10ad; 193 194 NSMutableDictionary* error_dict = 195 [NSMutableDictionary dictionaryWithCapacity:4]; 196 if (bundle_id) { 197 [error_dict setObject:bundle_id forKey:@"bundle_id"]; 198 } 199 if (version) { 200 [error_dict setObject:version forKey:@"version"]; 201 } 202 if (path) { 203 [error_dict setObject:path forKey:@"path"]; 204 } 205 NSURL* url_ns = base::mac::CFToNSCast(url_cf); 206 NSString* url_absolute_string = [url_ns absoluteString]; 207 if (url_absolute_string) { 208 [error_dict setObject:url_absolute_string forKey:@"url"]; 209 } 210 211 *error = CFErrorCreate(NULL, 212 app_bundle_id, 213 kBundleLoadBlocked, 214 base::mac::NSToCFCast(error_dict)); 215 } 216 217 return FALSE; 218 } 219 220 // Not blocked. Call through to the original implementation. 221 return g_original_underscore_cfbundle_load_executable_and_return_error( 222 bundle, force_global, error); 223 } 224 225 } // namespace 226 227 void EnableCFBundleBlocker() { 228 mach_error_t err = mach_override_ptr( 229 reinterpret_cast<void*>(_CFBundleLoadExecutableAndReturnError), 230 reinterpret_cast<void*>(ChromeCFBundleLoadExecutableAndReturnError), 231 reinterpret_cast<void**>( 232 &g_original_underscore_cfbundle_load_executable_and_return_error)); 233 if (err != err_none) { 234 DLOG(WARNING) << "mach_override _CFBundleLoadExecutableAndReturnError: " 235 << err; 236 } 237 } 238 239 namespace { 240 241 struct AllowedBundle { 242 // The bundle identifier to permit. These are matched with a case-sensitive 243 // literal comparison. "Children" of the declared bundle ID are permitted: 244 // if bundle_id here is @"org.chromium", it would match both @"org.chromium" 245 // and @"org.chromium.Chromium". 246 NSString* bundle_id; 247 248 // If bundle_id should only be permitted as of a certain minimum version, 249 // this string defines that version, which will be compared to the bundle's 250 // version with a numeric comparison. If bundle_id may be permitted at any 251 // version, set minimum_version to nil. 252 NSString* minimum_version; 253 }; 254 255 } // namespace 256 257 bool IsBundleAllowed(NSString* bundle_id, NSString* version) { 258 // The list of bundles that are allowed to load. Before adding an entry to 259 // this list, be sure that it's well-behaved. Specifically, anything that 260 // uses mach_override 261 // (https://github.com/rentzsch/mach_star/tree/master/mach_override) must 262 // use version 51ae3d199463fa84548f466d649f0821d579fdaf (July 22, 2011) or 263 // newer. Products added to the list must not cause crashes. Entries should 264 // include the name of the product, URL, and the name and e-mail address of 265 // someone responsible for the product's engineering. To add items to this 266 // list, file a bug at http://crbug.com/new using the "Defect on Mac OS" 267 // template, and provide the bundle ID (or IDs) and minimum CFBundleVersion 268 // that's safe for Chrome to load, along with the necessary product and 269 // contact information. Whitelisted bundles in this list may be removed if 270 // they are found to cause instability or otherwise behave badly. With 271 // proper contact information, Chrome developers may try to contact 272 // maintainers to resolve any problems. 273 const AllowedBundle kAllowedBundles[] = { 274 // Google Authenticator BT 275 // Dave MacLachlan <dmaclach (a] google.com> 276 { @"com.google.osax.Google_Authenticator_BT", nil }, 277 278 // Default Folder X, http://www.stclairsoft.com/DefaultFolderX/ 279 // Jon Gotow <gotow (a] stclairsoft.com> 280 { @"com.stclairsoft.DefaultFolderX", @"4.4.3" }, 281 282 // MySpeed, http://www.enounce.com/myspeed 283 // Edward Bianchi <ejbianchi (a] enounce.com> 284 { @"com.enounce.MySpeed.osax", @"1201" }, 285 286 // SIMBL (fork), https://github.com/albertz/simbl 287 // Albert Zeyer <albzey (a] googlemail.com> 288 { @"net.culater.SIMBL", nil }, 289 290 // Smart Scroll, http://marcmoini.com/sx_en.html 291 // Marc Moini <marc (a] a9ff.com> 292 { @"com.marcmoini.SmartScroll", @"3.9" }, 293 294 // Bartender for Mac, http://www.macbartender.com 295 // Ben Surtees <ben (a] surteesstudios.com> 296 { @"com.surteesstudios.BartenderHelper", @"1.2.20" }, 297 { @"com.surteesstudios.BartenderHelperSeventy", @"1.2.20" }, 298 { @"com.surteesstudios.BartenderHelperBundle", @"1.2.20" }, 299 }; 300 301 for (size_t index = 0; index < arraysize(kAllowedBundles); ++index) { 302 const AllowedBundle& allowed_bundle = kAllowedBundles[index]; 303 NSString* allowed_bundle_id = allowed_bundle.bundle_id; 304 NSUInteger allowed_bundle_id_length = [allowed_bundle_id length]; 305 306 // Permit bundle identifiers that are exactly equal to the allowed 307 // identifier, as well as "children" of the allowed identifier. 308 if ([bundle_id isEqualToString:allowed_bundle_id] || 309 ([bundle_id length] > allowed_bundle_id_length && 310 [bundle_id characterAtIndex:allowed_bundle_id_length] == '.' && 311 [bundle_id hasPrefix:allowed_bundle_id])) { 312 NSString* minimum_version = allowed_bundle.minimum_version; 313 if (!minimum_version) { 314 // If the rule didn't declare any version requirement, the bundle is 315 // allowed to load. 316 return true; 317 } 318 319 if (!version) { 320 // If there wasn't any version but one was required, the bundle isn't 321 // allowed to load. 322 return false; 323 } 324 325 // A numeric search is appropriate for comparing version numbers. 326 NSComparisonResult result = [version compare:minimum_version 327 options:NSNumericSearch]; 328 return result != NSOrderedAscending; 329 } 330 } 331 332 // Nothing matched. 333 return false; 334 } 335 336 } // namespace mac 337 } // namespace common 338 } // namespace chrome 339