001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.io; 019 020import java.io.File; 021import java.io.FileDescriptor; 022import java.io.FileInputStream; 023import java.io.FileOutputStream; 024import java.io.IOException; 025import java.io.RandomAccessFile; 026 027import org.apache.hadoop.conf.Configuration; 028import org.apache.hadoop.fs.FSDataInputStream; 029import org.apache.hadoop.fs.FileSystem; 030import org.apache.hadoop.fs.Path; 031import org.apache.hadoop.fs.permission.FsPermission; 032import org.apache.hadoop.io.nativeio.Errno; 033import org.apache.hadoop.io.nativeio.NativeIO; 034import org.apache.hadoop.io.nativeio.NativeIOException; 035import org.apache.hadoop.io.nativeio.NativeIO.POSIX.Stat; 036import org.apache.hadoop.security.UserGroupInformation; 037 038import com.google.common.annotations.VisibleForTesting; 039 040/** 041 * This class provides secure APIs for opening and creating files on the local 042 * disk. The main issue this class tries to handle is that of symlink traversal. 043 * <br/> 044 * An example of such an attack is: 045 * <ol> 046 * <li> Malicious user removes his task's syslog file, and puts a link to the 047 * jobToken file of a target user.</li> 048 * <li> Malicious user tries to open the syslog file via the servlet on the 049 * tasktracker.</li> 050 * <li> The tasktracker is unaware of the symlink, and simply streams the contents 051 * of the jobToken file. The malicious user can now access potentially sensitive 052 * map outputs, etc. of the target user's job.</li> 053 * </ol> 054 * A similar attack is possible involving task log truncation, but in that case 055 * due to an insecure write to a file. 056 * <br/> 057 */ 058public class SecureIOUtils { 059 060 /** 061 * Ensure that we are set up to run with the appropriate native support code. 062 * If security is disabled, and the support code is unavailable, this class 063 * still tries its best to be secure, but is vulnerable to some race condition 064 * attacks. 065 * 066 * If security is enabled but the support code is unavailable, throws a 067 * RuntimeException since we don't want to run insecurely. 068 */ 069 static { 070 boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled(); 071 boolean canBeSecure = NativeIO.isAvailable(); 072 073 if (!canBeSecure && shouldBeSecure) { 074 throw new RuntimeException( 075 "Secure IO is not possible without native code extensions."); 076 } 077 078 // Pre-cache an instance of the raw FileSystem since we sometimes 079 // do secure IO in a shutdown hook, where this call could fail. 080 try { 081 rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw(); 082 } catch (IOException ie) { 083 throw new RuntimeException( 084 "Couldn't obtain an instance of RawLocalFileSystem."); 085 } 086 087 // SecureIO just skips security checks in the case that security is 088 // disabled 089 skipSecurity = !canBeSecure; 090 } 091 092 private final static boolean skipSecurity; 093 private final static FileSystem rawFilesystem; 094 095 /** 096 * Open the given File for random read access, verifying the expected user/ 097 * group constraints if security is enabled. 098 * 099 * Note that this function provides no additional security checks if hadoop 100 * security is disabled, since doing the checks would be too expensive when 101 * native libraries are not available. 102 * 103 * @param f file that we are trying to open 104 * @param mode mode in which we want to open the random access file 105 * @param expectedOwner the expected user owner for the file 106 * @param expectedGroup the expected group owner for the file 107 * @throws IOException if an IO error occurred or if the user/group does 108 * not match when security is enabled. 109 */ 110 public static RandomAccessFile openForRandomRead(File f, 111 String mode, String expectedOwner, String expectedGroup) 112 throws IOException { 113 if (!UserGroupInformation.isSecurityEnabled()) { 114 return new RandomAccessFile(f, mode); 115 } 116 return forceSecureOpenForRandomRead(f, mode, expectedOwner, expectedGroup); 117 } 118 119 /** 120 * Same as openForRandomRead except that it will run even if security is off. 121 * This is used by unit tests. 122 */ 123 @VisibleForTesting 124 protected static RandomAccessFile forceSecureOpenForRandomRead(File f, 125 String mode, String expectedOwner, String expectedGroup) 126 throws IOException { 127 RandomAccessFile raf = new RandomAccessFile(f, mode); 128 boolean success = false; 129 try { 130 Stat stat = NativeIO.POSIX.getFstat(raf.getFD()); 131 checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner, 132 expectedGroup); 133 success = true; 134 return raf; 135 } finally { 136 if (!success) { 137 raf.close(); 138 } 139 } 140 } 141 142 /** 143 * Opens the {@link FSDataInputStream} on the requested file on local file 144 * system, verifying the expected user/group constraints if security is 145 * enabled. 146 * @param file absolute path of the file 147 * @param expectedOwner the expected user owner for the file 148 * @param expectedGroup the expected group owner for the file 149 * @throws IOException if an IO Error occurred or the user/group does not 150 * match if security is enabled 151 */ 152 public static FSDataInputStream openFSDataInputStream(File file, 153 String expectedOwner, String expectedGroup) throws IOException { 154 if (!UserGroupInformation.isSecurityEnabled()) { 155 return rawFilesystem.open(new Path(file.getAbsolutePath())); 156 } 157 return forceSecureOpenFSDataInputStream(file, expectedOwner, expectedGroup); 158 } 159 160 /** 161 * Same as openFSDataInputStream except that it will run even if security is 162 * off. This is used by unit tests. 163 */ 164 @VisibleForTesting 165 protected static FSDataInputStream forceSecureOpenFSDataInputStream( 166 File file, 167 String expectedOwner, String expectedGroup) throws IOException { 168 final FSDataInputStream in = 169 rawFilesystem.open(new Path(file.getAbsolutePath())); 170 boolean success = false; 171 try { 172 Stat stat = NativeIO.POSIX.getFstat(in.getFileDescriptor()); 173 checkStat(file, stat.getOwner(), stat.getGroup(), expectedOwner, 174 expectedGroup); 175 success = true; 176 return in; 177 } finally { 178 if (!success) { 179 in.close(); 180 } 181 } 182 } 183 184 /** 185 * Open the given File for read access, verifying the expected user/group 186 * constraints if security is enabled. 187 * 188 * Note that this function provides no additional checks if Hadoop 189 * security is disabled, since doing the checks would be too expensive 190 * when native libraries are not available. 191 * 192 * @param f the file that we are trying to open 193 * @param expectedOwner the expected user owner for the file 194 * @param expectedGroup the expected group owner for the file 195 * @throws IOException if an IO Error occurred, or security is enabled and 196 * the user/group does not match 197 */ 198 public static FileInputStream openForRead(File f, String expectedOwner, 199 String expectedGroup) throws IOException { 200 if (!UserGroupInformation.isSecurityEnabled()) { 201 return new FileInputStream(f); 202 } 203 return forceSecureOpenForRead(f, expectedOwner, expectedGroup); 204 } 205 206 /** 207 * Same as openForRead() except that it will run even if security is off. 208 * This is used by unit tests. 209 */ 210 @VisibleForTesting 211 protected static FileInputStream forceSecureOpenForRead(File f, String expectedOwner, 212 String expectedGroup) throws IOException { 213 214 FileInputStream fis = new FileInputStream(f); 215 boolean success = false; 216 try { 217 Stat stat = NativeIO.POSIX.getFstat(fis.getFD()); 218 checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner, 219 expectedGroup); 220 success = true; 221 return fis; 222 } finally { 223 if (!success) { 224 fis.close(); 225 } 226 } 227 } 228 229 private static FileOutputStream insecureCreateForWrite(File f, 230 int permissions) throws IOException { 231 // If we can't do real security, do a racy exists check followed by an 232 // open and chmod 233 if (f.exists()) { 234 throw new AlreadyExistsException("File " + f + " already exists"); 235 } 236 FileOutputStream fos = new FileOutputStream(f); 237 boolean success = false; 238 try { 239 rawFilesystem.setPermission(new Path(f.getAbsolutePath()), 240 new FsPermission((short)permissions)); 241 success = true; 242 return fos; 243 } finally { 244 if (!success) { 245 fos.close(); 246 } 247 } 248 } 249 250 /** 251 * Open the specified File for write access, ensuring that it does not exist. 252 * @param f the file that we want to create 253 * @param permissions we want to have on the file (if security is enabled) 254 * 255 * @throws AlreadyExistsException if the file already exists 256 * @throws IOException if any other error occurred 257 */ 258 public static FileOutputStream createForWrite(File f, int permissions) 259 throws IOException { 260 if (skipSecurity) { 261 return insecureCreateForWrite(f, permissions); 262 } else { 263 return NativeIO.getCreateForWriteFileOutputStream(f, permissions); 264 } 265 } 266 267 private static void checkStat(File f, String owner, String group, 268 String expectedOwner, 269 String expectedGroup) throws IOException { 270 boolean success = true; 271 if (expectedOwner != null && 272 !expectedOwner.equals(owner)) { 273 if (Path.WINDOWS) { 274 UserGroupInformation ugi = 275 UserGroupInformation.createRemoteUser(expectedOwner); 276 final String adminsGroupString = "Administrators"; 277 success = owner.equals(adminsGroupString) 278 && ugi.getGroups().contains(adminsGroupString); 279 } else { 280 success = false; 281 } 282 } 283 if (!success) { 284 throw new IOException( 285 "Owner '" + owner + "' for path " + f + " did not match " + 286 "expected owner '" + expectedOwner + "'"); 287 } 288 } 289 290 /** 291 * Signals that an attempt to create a file at a given pathname has failed 292 * because another file already existed at that path. 293 */ 294 public static class AlreadyExistsException extends IOException { 295 private static final long serialVersionUID = 1L; 296 297 public AlreadyExistsException(String msg) { 298 super(msg); 299 } 300 301 public AlreadyExistsException(Throwable cause) { 302 super(cause); 303 } 304 } 305}