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