windows-nt/Source/XPSP1/NT/enduser/windows.com/lib/wusafefn/safefile.cpp

1034 lines
27 KiB
C++
Raw Normal View History

2020-09-26 03:20:57 -05:00
//
// 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 <strsafe.h>
//
// 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;
}