// // SafeFile.cpp // // Functions to help prevent opening unsafe files. // // History: // // 2002-03-18 KenSh Created // // Copyright (c) 2002 Microsoft Corporation // #include "stdafx.h" #include "SafeFile.h" #include // // Hopefully most projects already define these; if not, ensure we still compile // #ifndef ASSERT #define ASSERT(x) #endif #ifndef ARRAYSIZE #define ARRAYSIZE(ar) (sizeof(ar)/sizeof((ar)[0])) #endif // // Eliminate an unnecessary function call on Unicode builds // #ifndef CHARNEXT #ifdef UNICODE #define CHARNEXT(psz) (psz+1) #else #define CHARNEXT CharNextA #endif #endif // // Local function declarations // static inline BOOL IsSlashOrBackslash(IN TCHAR ch); static inline BOOL IsSlashOrBackslash(IN TCHAR ch); static BOOL SkipLangNeutralPrefix(IN LPCTSTR pszString, IN LPCTSTR pszPrefix, OUT LPCTSTR* ppszResult); static BOOL MyPathFindNextComponent(IN LPCTSTR pszFileName, IN BOOL fAllowForwardSlash, OUT LPCTSTR* ppszResult); static BOOL SkipPathDrivePart(IN LPCTSTR pszFileName, OUT OPTIONAL int* pcchDrivePart, OUT OPTIONAL BOOL* pfUNC, OUT OPTIONAL BOOL* pfExtendedSyntax); static HRESULT CheckValidDriveType(IN LPCTSTR pszFileName, IN BOOL fAllowNetworkDrive, IN BOOL fAllowRemovableDrive); static BOOL WINAPI DoesPathContainDotDot(IN LPCTSTR pszFileName); static BOOL DoesPathContainStreamSyntax(IN LPCTSTR pszFileName); static HRESULT CheckReparsePointPermissions(IN DWORD dwReparseType); //============================================================================ static inline HRESULT GetLastErrorAsHresult() { DWORD dwErr = GetLastError(); return HRESULT_FROM_WIN32(dwErr); } // IsSlashOrBackslash [private] // // Helper function to simplify code that checks for path separators. // Most places where backslash is valid, forward slash is also valid. // static inline BOOL IsSlashOrBackslash(IN TCHAR ch) { return (ch == _T('\\') || ch == _T('/')); } // StrLenWithMax [private] // // Returns the equivalent of min(lstrlen(pszString), cchMax) // but avoids most of the lstrlen when cchMax is small. // static int StrLenWithMax(IN LPCTSTR pszString, IN int cchMax) { int cch = 0; while (*pszString && cch < cchMax) cch++; return cch; } // SkipLangNeutralPrefix [private] // // Sets the out param to the new string pointer after skipping the prefix, // if the string starts with the prefix (case-insensitive). Otherwise sets // the out param to the start of the input string. // // Returns TRUE if the prefix was found & skipped, otherwise FALSE. // static BOOL SkipLangNeutralPrefix(IN LPCTSTR pszString, IN LPCTSTR pszPrefix, OUT LPCTSTR* ppszResult) { int cchPrefix = lstrlen(pszPrefix); int cchString = StrLenWithMax(pszString, cchPrefix); BOOL fResult = FALSE; if (CSTR_EQUAL == CompareString(MAKELCID(LANG_ENGLISH, SORT_DEFAULT), NORM_IGNORECASE, pszString, cchString, pszPrefix, cchPrefix)) { fResult = TRUE; pszString += cchPrefix; } *ppszResult = pszString; return fResult; } // MyPathFindNextComponent [private] // // Skips past the next component of the given path, including the slash or // backslash that follows it. // // Sets the out param to the beginning of the next path component, or to // the end of string if there is no next path component. // // Returns TRUE if a slash or backslash was found and skipped. Note that // the out param can be "" even if function returns TRUE. // static BOOL MyPathFindNextComponent ( IN LPCTSTR pszFileName, IN BOOL fAllowForwardSlash, OUT LPCTSTR* ppszResult ) { // This is a string-parsing helper function; params should never be NULL ASSERT(pszFileName != NULL); ASSERT(ppszResult != NULL); LPCTSTR pszStart = pszFileName; TCHAR chSlash2 = (fAllowForwardSlash ? _T('/') : _T('\\')); BOOL fResult = FALSE; for (;;) { TCHAR ch = *pszFileName; if (ch == _T('\0')) break; // didn't find a path separator; we'll return FALSE // Advance to next char, even if current char is path separator (\ or /) pszFileName = CHARNEXT(pszFileName); if (ch == _T('\\') || ch == chSlash2) { fResult = TRUE; break; } } *ppszResult = pszFileName; return fResult; } // SkipPathDrivePart [private] // // Parses the filename to determine the length of the base drive portion of // the filename, and to determine what syntax the name is in. // // This function does not actually examine the drive or file to ensure existence, // or to recognize that a drive letter like X:\ might be a network drive. // // Returns: // TRUE - if the input is a full path // FALSE - if input param is not a full path, or is bogus. The pcchDrivePart // out param is set to 0 in this case. // static BOOL SkipPathDrivePart ( IN LPCTSTR pszFileName, // input path name (full or relative path) OUT OPTIONAL int* pcchDrivePart, // # of TCHARs used by drive part OUT OPTIONAL BOOL* pfUNC, // TRUE if path is UNC (not incl mapped drive) OUT OPTIONAL BOOL* pfExtendedSyntax // TRUE if path is \\?\ syntax ) { BOOL fFullPath = FALSE; LPCTSTR pszOriginalFileName = pszFileName; int fUNC = FALSE; int fExtendedSyntax = FALSE; if (!pszFileName) goto done; // BLOCK { // // Skip \\?\ if present. (This part must use backslashes, not forward slashes) // #ifdef UNICODE if (SkipLangNeutralPrefix(pszFileName, _T("\\\\?\\"), &pszFileName)) { fExtendedSyntax = TRUE; if (SkipLangNeutralPrefix(pszFileName, _T("UNC\\"), &pszFileName)) { fUNC = TRUE; // Found "\\?\UNC\..." } else if (SkipLangNeutralPrefix(pszFileName, _T("Volume{"), &pszFileName)) { // Found "\\?\Volume{1f3b3813-ddbf-11d5-ab2e-806d6172696f}\". // Skip the rest of the volume name. fFullPath = MyPathFindNextComponent(pszFileName, FALSE, &pszFileName); goto done; } // else continue normal parsing starting at updated pszFileName pointer } #endif // UNICODE // // Check for path of the form C:\ // TCHAR chFirstUpper = (TCHAR)CharUpper((LPTSTR)(pszFileName[0])); if (chFirstUpper >= _T('A') && chFirstUpper <= _T('Z') && pszFileName[1] == _T(':') && pszFileName[2] == _T('\\')) { pszFileName += 3; fFullPath = TRUE; goto done; } // // Check for UNC of the form \\server\share\ // if (!fExtendedSyntax && pszFileName[0] == _T('\\') && pszFileName[1] == _T('\\')) { fUNC = TRUE; pszFileName += 2; // skip the "\\" } if (fUNC) // may be \\server\share\ or \\?\UNC\server\share\ { // Skip past server and share names. Trailing backslash is NOT optional. if (!MyPathFindNextComponent(pszFileName, TRUE, &pszFileName) || !MyPathFindNextComponent(pszFileName, TRUE, &pszFileName)) { goto done; // incomplete UNC path -> return failure } fFullPath = TRUE; } } done: if (pcchDrivePart) *pcchDrivePart = fFullPath ? (int)(pszFileName - pszOriginalFileName) : 0; if (pfUNC) *pfUNC = fUNC; if (pfExtendedSyntax) *pfExtendedSyntax = fExtendedSyntax; return fFullPath; } // GetReparsePointType [public] // // Given the full path of a file or directory, determines what type of // reparse point the path represents. // // Returns S_OK if the type of reparse point could be determined, or // an appropriate error code if not. // // The out param is set to the reparse point type, or 0 if none. // The value for both volume mount points and junction points is // IO_REPARSE_TAG_MOUNT_POINT. (Use GetVolumeNameForVolumeMountPoint // to distinguish, if necessary.) // HRESULT WINAPI GetReparsePointType ( IN LPCTSTR pszFileName, // full path to folder to check OUT DWORD* pdwReparsePointType // set to reparse point type, or 0 if none ) { HRESULT hr = S_OK; DWORD dwReparseType = 0; ASSERT(pdwReparsePointType); // BLOCK { if (!pszFileName) { hr = E_INVALIDARG; goto done; } DWORD dwAttrib = GetFileAttributes(pszFileName); if (dwAttrib == INVALID_FILE_ATTRIBUTES) goto win32_error; if (dwAttrib & FILE_ATTRIBUTE_REPARSE_POINT) { WIN32_FIND_DATA Find; HANDLE hFind = FindFirstFile(pszFileName, &Find); if (hFind == INVALID_HANDLE_VALUE) goto win32_error; dwReparseType = Find.dwReserved0; FindClose(hFind); } goto done; } win32_error: hr = GetLastErrorAsHresult(); done: *pdwReparsePointType = dwReparseType; ASSERT(hr != E_INVALIDARG); return hr; } // CheckReparsePointPermissions [private] // // Determines whether or not it's ok to trust the given reparse type. // Returns S_OK if it's safe, or an appropriate error message if not. // static HRESULT CheckReparsePointPermissions(IN DWORD dwReparseType) { HRESULT hr = S_OK; // REVIEW: Any reason to worry about these other types of reparse points? // IO_REPARSE_TAG_HSM, IO_REPARSE_TAG_SIS, IO_REPARSE_TAG_DFS, etc. if (dwReparseType == IO_REPARSE_TAG_MOUNT_POINT) { hr = HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED); } return hr; } // CheckValidDriveType [private] // // Gets the volume name associated with the given file, and checks the // return value from GetDriveType() to see whether or not operations // are allowed on the file. // static HRESULT CheckValidDriveType ( IN LPCTSTR pszFileName, // full path of a file whose drive we want to check IN BOOL fAllowNetworkDrive, // determines whether or not net drives are allowed IN BOOL fAllowRemovableDrive // determines whether or not removable drives are allowed ) { HRESULT hr = E_INVALIDARG; LPTSTR pszVolumePath = NULL; // BLOCK { if (!pszFileName) { goto done; // hr is already E_INVALIDARG } int cchFileName = lstrlen(pszFileName); pszVolumePath = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * (cchFileName+1)); if (!pszVolumePath) { hr = E_OUTOFMEMORY; goto done; } #ifdef UNICODE if (!GetVolumePathName(pszFileName, pszVolumePath, cchFileName+1)) { hr = GetLastErrorAsHresult(); goto done; } #else int cchDrivePart; if (!SkipPathDrivePart(pszFileName, &cchDrivePart, NULL, NULL)) { hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); goto done; } StringCchCopy(pszVolumePath, cchDrivePart+1, pszFileName); #endif UINT uDriveType = GetDriveType(pszVolumePath); switch (uDriveType) { case DRIVE_FIXED: hr = S_OK; break; case DRIVE_REMOVABLE: case DRIVE_CDROM: case DRIVE_UNKNOWN: case DRIVE_RAMDISK: hr = fAllowRemovableDrive ? S_OK : E_ACCESSDENIED; break; case DRIVE_REMOTE: hr = fAllowNetworkDrive ? S_OK : E_ACCESSDENIED; break; default: hr = E_INVALIDARG; break; } } done: SafeFileFree(pszVolumePath); ASSERT(hr != E_INVALIDARG); return hr; } // IsFullPathName [public] // // Determines whether the given filename is a full path including a drive // or UNC. Filenames such as \\?\ are supported, and can be considered // valid or not depending on the dwSafeFlags parameter. // // Returns: // TRUE - if the filename is a full path // FALSE - if filename is NULL, isn't a full path, or fails to meet // the criteria given in the dwSafeFlags parameter. // BOOL WINAPI IsFullPathName ( IN LPCTSTR pszFileName, // full or relative path to a file OUT OPTIONAL BOOL* pfUNC, // TRUE path is UNC (int incl mapped drive) OUT OPTIONAL BOOL* pfExtendedSyntax // TRUE if path is \\?\ syntax ) { return SkipPathDrivePart(pszFileName, NULL, pfUNC, pfExtendedSyntax); } // DoesPathContainDotDot [private] // // Returns TRUE if the path contains any ".." references, else FALSE. // static BOOL WINAPI DoesPathContainDotDot(IN LPCTSTR pszFileName) { if (!pszFileName) return FALSE; while (*pszFileName) { // Flag path components that consist exactly of ".." (nothing following) if (pszFileName[0] == _T('.') && pszFileName[1] == _T('.') && (pszFileName[2] == _T('/') || pszFileName[2] == _T('\\') || pszFileName[2] == _T('\0'))) { return TRUE; } MyPathFindNextComponent(pszFileName, TRUE, &pszFileName); } return FALSE; } // DoesPathContainStreamSyntax [private] // // Returns TRUE if the path contains any characters that could cause it // to refer to an alternate NTFS stream (namely any ":" characters beyond // the drive specification). // static BOOL DoesPathContainStreamSyntax(IN LPCTSTR pszFileName) { if (!pszFileName) return FALSE; int cchSkip; SkipPathDrivePart(pszFileName, &cchSkip, NULL, NULL); for (LPCTSTR pch = pszFileName + cchSkip; *pch; pch = CHARNEXT(pch)) { if (*pch == _T(':')) return TRUE; } return FALSE; } // SafeCreateFile [public] // // Opens the given file, ensuring that it meets certain path standards (e.g. // doesn't contain "..") and that it is a file, not a device or named pipe. // HRESULT WINAPI SafeCreateFile ( OUT HANDLE* phFileResult, // receives handle to opened file, or INVALID_HANDLE_VALUE IN DWORD dwSafeFlags, // zero or more SCF_* flags IN LPCTSTR pszFileName, // same as CreateFile IN DWORD dwDesiredAccess, // same as CreateFile IN DWORD dwShareMode, // same as CreateFile IN LPSECURITY_ATTRIBUTES lpSecurityAttributes, // same as CreateFile IN DWORD dwCreationDisposition, // same as CreateFile IN DWORD dwFlagsAndAttributes, // same as CreateFile + (SECURITY_SQOS_PRESENT|SECURITY_ANONYMOUS) IN HANDLE hTemplateFile // same as CreateFile ) { HANDLE hFile = INVALID_HANDLE_VALUE; HRESULT hr = S_OK; // BLOCK { if (!pszFileName || !phFileResult || (dwSafeFlags & ~(SCF_ALLOW_NETWORK_DRIVE | SCF_ALLOW_REMOVABLE_DRIVE | SCF_ALLOW_ALTERNATE_STREAM))) { hr = E_INVALIDARG; goto done; } // We require a full pathname. if (!IsFullPathName(pszFileName)) { hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); goto done; } // Ensure path doesn't contain ".." references if (DoesPathContainDotDot(pszFileName)) { hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME); goto done; } // Ensure filename doesn't refer to alternate stream unless allowed if (!(dwSafeFlags & SCF_ALLOW_ALTERNATE_STREAM) && DoesPathContainStreamSyntax(pszFileName)) { hr = HRESULT_FROM_WIN32(ERROR_INVALID_NAME); goto done; } // Check drive type to ensure it's allowed by dwSafeFlags if (FAILED(hr = CheckValidDriveType(pszFileName, (dwSafeFlags & SCF_ALLOW_NETWORK_DRIVE), (dwSafeFlags & SCF_ALLOW_REMOVABLE_DRIVE)))) { goto done; } // Open the file w/ extra security attributes dwFlagsAndAttributes |= (SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS); hFile = CreateFile(pszFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); if (hFile == INVALID_HANDLE_VALUE) { goto win32_error; } // Ensure it's really a file if (FILE_TYPE_DISK != GetFileType(hFile)) { CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; hr = HRESULT_FROM_WIN32(ERROR_OPEN_FAILED); } goto done; } // end BLOCK win32_error: hr = GetLastErrorAsHresult(); done: if (phFileResult) *phFileResult = hFile; ASSERT(hr != E_INVALIDARG); return hr; } // SafeRemoveFileAttributes [public] // // Given a filename and that file's current attributes, checks whether // any of the bits in dwRemoveAttrib need to be removed from the file, // and if necessary calls SetFileAttributes() to remove them. // // Designed to check for invalid dwCurAttrib and call GetLastError() // for you, so you can pass GetFileAttributes() directly as a parameter. // HRESULT WINAPI SafeRemoveFileAttributes ( IN LPCTSTR pszFileName, // full path to file whose attributes we will change IN DWORD dwCurAttrib, // current attributes of the file IN DWORD dwRemoveAttrib // attribute bits to remove ) { HRESULT hr = S_OK; // this is default if attrib doesn't need to be removed if (!pszFileName || !dwRemoveAttrib) { hr = E_INVALIDARG; goto done; } if (dwCurAttrib & dwRemoveAttrib) // note: always true if dwCurAttrib==INVALID_FILE_ATTRIBUTES { if (dwCurAttrib == INVALID_FILE_ATTRIBUTES || !SetFileAttributes(pszFileName, dwCurAttrib & ~dwRemoveAttrib)) { hr = GetLastErrorAsHresult(); } } done: ASSERT(hr != E_INVALIDARG); return hr; } // SafeDeleteFolderAndContentsHelper [private] // // Does all work except the parameter validation for for // SafeDeleteFolderAndContents. // static HRESULT SafeDeleteFolderAndContentsHelper ( IN LPCTSTR pszFolderToDelete, // folder in current level of recursion IN DWORD dwSafeFlags, // zero or more SDF_* flags OUT WIN32_FIND_DATA* pFind // struct to use for FindFirst/FindNext (to avoid malloc) ) { HRESULT hr = S_OK; LPTSTR pszCurFile = NULL; HANDLE hFind = INVALID_HANDLE_VALUE; // Allocate room for folder + backslash + MAX_PATH (includes trailing null) int cchFolderName = lstrlen(pszFolderToDelete); int cchAllocCurFile = cchFolderName + 1 + MAX_PATH; pszCurFile = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * cchAllocCurFile); if (!pszCurFile) { hr = E_OUTOFMEMORY; goto done; } // Check for read-only base folder if (dwSafeFlags & SDF_DELETE_READONLY_FILES) { hr = SafeRemoveFileAttributes(pszFolderToDelete, GetFileAttributes(pszFolderToDelete), FILE_ATTRIBUTE_READONLY); if (FAILED(hr) && !(dwSafeFlags & SDF_CONTINUE_IF_ERROR)) goto done; } // Build search path by appending "\*.*" StringCchCopy(pszCurFile, cchAllocCurFile, pszFolderToDelete); if (!IsSlashOrBackslash(pszCurFile[cchFolderName-1])) pszCurFile[cchFolderName++] = _T('\\'); StringCchCopy(pszCurFile + cchFolderName, cchAllocCurFile - cchFolderName, _T("*.*")); // Iterate through all files in this folder hFind = FindFirstFile(pszCurFile, pFind); if (hFind == INVALID_HANDLE_VALUE) { hr = GetLastErrorAsHresult(); // probably doesn't exist, or not a folder goto done; } else { do { if (0 == lstrcmp(pFind->cFileName, _T(".")) || 0 == lstrcmp(pFind->cFileName, _T(".."))) { continue; } StringCchCopy(pszCurFile + cchFolderName, cchAllocCurFile - cchFolderName, pFind->cFileName); HRESULT hrCur = S_OK; if (!(pFind->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || SUCCEEDED(hrCur = CheckReparsePointPermissions(pFind->dwReserved0))) { // Remove read-only attribute if allowed if (dwSafeFlags & SDF_DELETE_READONLY_FILES) { hrCur = SafeRemoveFileAttributes(pszCurFile, pFind->dwFileAttributes, FILE_ATTRIBUTE_READONLY); } if (SUCCEEDED(hrCur) || (dwSafeFlags & SDF_CONTINUE_IF_ERROR)) { HRESULT hrCur2 = S_OK; if (pFind->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // Recursively delete folder and contents // Note that pFind's contents are clobbered by this call hrCur2 = SafeDeleteFolderAndContentsHelper(pszCurFile, dwSafeFlags, pFind); } else { // Delete the file if (!DeleteFile(pszCurFile)) { hrCur2 = GetLastErrorAsHresult(); } } if (FAILED(hrCur2)) hrCur = hrCur2; } } if (FAILED(hrCur)) hr = hrCur; if (FAILED(hr) && !(dwSafeFlags & SDF_CONTINUE_IF_ERROR)) goto done; } while (FindNextFile(hFind, pFind)); FindClose(hFind); hFind = INVALID_HANDLE_VALUE; } // Delete the folder if (!RemoveDirectory(pszFolderToDelete)) { if (SUCCEEDED(hr)) hr = GetLastErrorAsHresult(); } done: if (hFind != INVALID_HANDLE_VALUE) FindClose(hFind); SafeFileFree(pszCurFile); return hr; } // SafeDeleteFolderAndContents [public] // // Deletes the given folder and all of its contents, but refuses to walk // across reparse points. // HRESULT WINAPI SafeDeleteFolderAndContents ( IN LPCTSTR pszFolderToDelete, // full path of folder to delete IN DWORD dwSafeFlags // zero or more SDF_* flags ) { HRESULT hr = E_INVALIDARG; if (!pszFolderToDelete || !(*pszFolderToDelete) || (dwSafeFlags & ~(SDF_ALLOW_NETWORK_DRIVE | SDF_DELETE_READONLY_FILES | SDF_CONTINUE_IF_ERROR))) { goto done; // hr already set to E_INVALIDARG } // // Ensure it's a full path, but not the root of a drive // int cchDrivePart; if (!SkipPathDrivePart(pszFolderToDelete, &cchDrivePart, NULL, NULL) || pszFolderToDelete[cchDrivePart] == _T('\0')) { hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME); goto done; } // // Ensure we're not deleting from a network drive unless allowed // if (FAILED(hr = CheckValidDriveType(pszFolderToDelete, (dwSafeFlags & SDF_ALLOW_NETWORK_DRIVE), TRUE))) { goto done; } // // Ensure starting point is not a reparse point // DWORD dwReparseType; if (FAILED(hr = GetReparsePointType(pszFolderToDelete, &dwReparseType)) || FAILED(hr = CheckReparsePointPermissions(dwReparseType))) { goto done; } WIN32_FIND_DATA Find; hr = SafeDeleteFolderAndContentsHelper(pszFolderToDelete, dwSafeFlags, &Find); done: ASSERT(hr != E_INVALIDARG); return hr; } // SafeFileCheckForReparsePoint [public] // // Checks a subset of the given filename's component parts to ensure that // they are not reparse points (specifically, volume mount points or // junction points: see linkd.exe and mountvol.exe). // // Normal return values are S_OK or HRESULT_FROM_WIN32(ERROR_REPARSE_TAG_MISMATCH). // Other values may be returned in exceptional cases such as out-of-memory. // HRESULT WINAPI SafeFileCheckForReparsePoint ( IN LPCTSTR pszFileName, // full path of a file IN int nFirstUntrustedOffset, // char offset of first path component to check IN DWORD dwSafeFlags // zero or more SRP_* flags ) { HRESULT hr = E_INVALIDARG; LPTSTR pszMutableFileName = NULL; // BLOCK { if (!pszFileName || (dwSafeFlags & ~SRP_FILE_MUST_EXIST)) { goto done; // hr is already E_INVALIDARG } int cchFileName = lstrlen(pszFileName); if ((UINT)nFirstUntrustedOffset >= (UINT)cchFileName) // bad offset, or zero-length filename { goto done; // hr is already E_INVALIDARG } pszMutableFileName = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * (cchFileName+1)); if (!pszMutableFileName) { hr = E_OUTOFMEMORY; goto done; } StringCchCopy(pszMutableFileName, cchFileName+1, pszFileName); // // Always consider the drive part of the path to be trusted // int cchDrivePart; if (!SkipPathDrivePart(pszMutableFileName, &cchDrivePart, NULL, NULL)) { hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME); goto done; } if (nFirstUntrustedOffset < cchDrivePart) nFirstUntrustedOffset = cchDrivePart; // // Validate left-to-right, starting after trusted base path // LPTSTR pszNextComponent = pszMutableFileName + nFirstUntrustedOffset; BOOL fMoreComponents = TRUE; do { // // Advance pszNextComponent; truncate after current path component // fMoreComponents = MyPathFindNextComponent(pszNextComponent, TRUE, (LPCTSTR*)&pszNextComponent); TCHAR chSave = *(pszNextComponent-1); if (fMoreComponents) { *(pszNextComponent-1) = _T('\0'); } // Get reparse point type of truncated string, and undo the truncation DWORD dwReparseType; if (FAILED(hr = GetReparsePointType(pszMutableFileName, &dwReparseType))) goto done; *(pszNextComponent-1) = chSave; // Check for forbidden reparse point type, e.g. mounted drive if (FAILED(hr = CheckReparsePointPermissions(dwReparseType))) goto done; } while (fMoreComponents); } // end BLOCK done: SafeFileFree(pszMutableFileName); // Ignore file-not-found errors, if requested in dwSafeFlags if (!(dwSafeFlags & SRP_FILE_MUST_EXIST) && (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))) { hr = S_OK; } ASSERT(hr != E_INVALIDARG); return hr; } // SafePathCombine [public] // // Combines a path and filename, ensuring exactly one backslash between them. // The second "untrusted" half of the path is checked to ensure that it is // safe (doesn't contain ".." or ":", or point to existing reparse points). // // File-not-found errors are ignored unless SPC_FILE_MUST_EXIST flag is specified. // // It's ok for the base path and the output buffer to point to the same buffer. // // Returns S_OK if successful, or an appropriate error code if not. // HRESULT WINAPI SafePathCombine ( OUT LPTSTR pszBuf, // buffer where combined path will be stored IN int cchBuf, // size of output buffer, in TCHARs IN LPCTSTR pszTrustedBasePath, // first half of path, all trusted IN LPCTSTR pszUntrustedFileName, // second half of path, not trusted IN DWORD dwSafeFlags // zero or more SPC_* flags ) { HRESULT hr = E_INVALIDARG; if (!pszBuf || cchBuf <= 0 || !pszTrustedBasePath || !pszUntrustedFileName || (dwSafeFlags & ~(SPC_FILE_MUST_EXIST | SPC_ALLOW_ALTERNATE_STREAM))) { goto done; // hr is already E_INVALIDARG } // BLOCK { int cchBasePath = lstrlen(pszTrustedBasePath); int cchFileName = lstrlen(pszUntrustedFileName); if (cchBasePath == 0 || cchFileName == 0) { goto done; // hr is already E_INVALIDARG } // Ensure nothing bogus in the untrusted part of the filename if (DoesPathContainDotDot(pszUntrustedFileName)) { hr = ERROR_BAD_PATHNAME; goto done; } if (!(dwSafeFlags & SPC_ALLOW_ALTERNATE_STREAM) && DoesPathContainStreamSyntax(pszUntrustedFileName)) { hr = HRESULT_FROM_WIN32(ERROR_INVALID_NAME); goto done; } // // Ensure room for the "\" that will be inserted. // int cchInsertSlash = 0; if (!IsSlashOrBackslash(pszTrustedBasePath[cchBasePath-1])) { cchInsertSlash = 1; } if (cchBasePath + cchInsertSlash + cchFileName >= cchBuf) { hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); goto done; } // // Build full path with a backslash between // if (pszBuf != pszTrustedBasePath) StringCchCopy(pszBuf, cchBuf, pszTrustedBasePath); int cchUsed = cchBasePath; if (cchInsertSlash > 0) { pszBuf[cchUsed++] = _T('\\'); } StringCchCopy(pszBuf + cchUsed, cchBuf - cchUsed, pszUntrustedFileName); // // Ensure no junctions or volume mount points in untrusted portion // DWORD dwReparseFlags = (dwSafeFlags & SPC_FILE_MUST_EXIST) ? SRP_FILE_MUST_EXIST : 0; hr = SafeFileCheckForReparsePoint(pszBuf, cchUsed, dwReparseFlags); } done: if (FAILED(hr) && pszBuf && cchBuf > 0) pszBuf[0] = _T('\0'); ASSERT(hr != E_INVALIDARG); return hr; } // SafePathCombineAlloc [public] // // See comments for SafePathCombine. The only difference is that this // function allocates a buffer of sufficient size and stores it in the // output parameter ppszResult. Caller is responsible for freeing the // buffer via SafeFileFree. // HRESULT WINAPI SafePathCombineAlloc ( OUT LPTSTR* ppszResult, // ptr to newly alloc'd buffer stored here IN LPCTSTR pszTrustedBasePath, // first half of path, all trusted IN LPCTSTR pszUntrustedFileName, // second half of path, not trusted IN DWORD dwSafeFlags // zero or more SPC_* flags ) { HRESULT hr = E_INVALIDARG; ASSERT(ppszResult); *ppszResult = NULL; if (!pszTrustedBasePath || !pszUntrustedFileName) { goto done; // hr already set to E_INVALIDARG } // Allocate room for the max possible length (includes room for "\" between parts) int cchMaxNeeded = lstrlen(pszTrustedBasePath) + lstrlen(pszUntrustedFileName) + 2; LPTSTR pszResult = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * cchMaxNeeded); if (!pszResult) { hr = E_OUTOFMEMORY; goto done; } hr = SafePathCombine(pszResult, cchMaxNeeded, pszTrustedBasePath, pszUntrustedFileName, dwSafeFlags); if (FAILED(hr)) { SafeFileFree(pszResult); } else { *ppszResult = pszResult; } done: ASSERT(hr != E_INVALIDARG); return hr; }