Security Best Practices

In the 3ds Max 2020 SDK we introduced the header banned.h to help plug-in developers identify where their code uses potentially insecure Windows APIs. The 3ds Max team uses these same headers to improve the security of 3ds Max, and third party developers are encouraged to consider using this resource to reduce vulnerabilities in their code.

Note:

The implementation of banned.h is a continuing effort. Not all code in the 3ds Max SDK has been compiled with banned.h, so it is likely that there are 3ds Max APIs that will not work when compiled with this header. Third party developers can start using banned.h where appropriate, but may not be able to fully implement it for plug-ins yet.

There are two additional headers in the 3ds Max SDK to support banned.h:

Overview of banned.h and 3dsmax_banned.h

Microsoft describes banned.h as "a sanitizing resource that is designed to help developers avoid using and help identify and remove banned functions from code that may lead to vulnerabilities. Banned functions are those calls in code that have been deemed dangerous by making it relatively easy to introduce vulnerabilities into code during development."

See the Article on banned.h.

The 3dsmax_banned.h is a minimal wrapper for banned.h intended as a common starting point to compile 3ds Max plug-ins in Visual Studio. It pre-includes a minimal set of common system and 3rd-party library headers, to ignore compilation errors caused by non-compliant external code. It conditionally pre-includes a non-compliant subset of Qt headers (added to 3dsmax_banned.h to avoid redundant code, since many projects depend on Qt).

You can create your own project-specific wrapper around 3dsmax_banned.h to ignore other non-compliant third-party headers, or to temporarily ignore those of your own headers that are not yet fixed. Additionally, you can use this header to configure custom function deprecations which are not detected by banned.h.

Using 3dsmax_banned.h

There are two ways to add 3dsmax_banned.h to your project:

  1. Use the "Forced Include File" compiler option (/FI) to automatically force include the header in every .cpp file in the project. You can either enable /FI using the project Property Pages for each build configuration, or by adding this section to your .vcxproj file:
<ItemDefinitionGroup>
  <ClCompile>    
    <ForcedIncludeFiles>3dsmax_banned.h</ForcedIncludeFiles>
  </ClCompile>
</ItemDefinitionGroup>
  1. Manually #include the header in each .cpp file after all other #include directives. This approach requires more maintenance, but can be used to fix one file at a time without generating an overwhelming number of errors.

Integrating 3dsmax_banned.h

  1. Check the project-level build configuration and modify if needed: Enable the /sdl compiler option. This enables treating C4995 warnings (deprecated insecure functions) as errors. Check that the C4995 warning is not disabled by other project settings, or by #pragma warning(disable) directives.
    Note:

    The 3dsmax.common.tools.settings.props property sheet already enables the /sdl option.

  2. If you are manually including 3dsmax_banned.h (or your own wrapper header), add it to the .cpp files you want to check after all other #include directives.
  3. Rebuild the project and analyze any resulting errors. See Fixing Errors below.

Fixing Errors

We can classify errors as either "side-effect" errors (errors that are not directly attributable to deprecated insecure functions), and actual C4995 function deprecation errors.

Fixing "Side-effect" Errors

It is a good idea to fix or suppress these various "side-effect" errors to reduce noise before dealing with C4995 errors.

Macro redefinition errors: Adding 3dsmax_banned.h to a project can change the overall inclusion order of header files, which can lead to macro redefinition errors. This is where two unrelated headers use the same name to define different macros. Isolate a conflicting macro definition within its library header file by surrounding the affected #include directive with #pragma push_macro and pop_macro directives. Then, add the encapsulated include directive to the appropriate banned.h wrapper header.

Here is an example of handling a conflicting macro called EPS:

// Pre-include the main Example SDK header (included by "dll/MaxExample/ExampleSim.h")
#include <Example.h>  // Note: "banned" function calls are actually located in nested header "Example/Example/common/include/core/ExampleParamExtractor.h"

// Include other Example/Example headers to avoid macro conflicts errors
#pragma push_macro("EPS") // conflicting macro definitions between <Examplemath/bbox.h> and <maxscript\maxscript.h>
#undef EPS
#include <Exampleapi/Example_math.h> // Note: macro conflict is actually located in nested header <Examplemath/bbox.h>
#pragma pop_macro("EPS")


#include <3dsmax_banned.h>

Redundant macro definition errors can show up if your project .cpp files define the STRICT macro, which 3dsmax_banned.h already defines before pre-including windows.h. You can fix these errors by removing the STRICT macro definitions from your project files, or surround them with #ifdef / #endif directives.

Qt Related Warnings: Many Qt 3rd-party library headers do not compile with warning level 4, which can result in numerous compilation warnings (for example 4127, 4251, 4800, 4275, and 4244). Ideally, compilation warnings that originate in Qt 3rd-party headers should be suppressed locally, only where needed, by surrounding the affected Qt header includes with #pragma warning push/disable/pop directives. For example (from maxsdk/include/Qt/QmaxTranslationLoader.h):

#include <utilexp.h>

#pragma warning( push )
#pragma warning( disable: 4127 4251 4800 )
#include <QtCore/QObject>
#include <QtCore/QString>
#pragma warning( pop )

However, this can require significant effort, especially in projects with a large number of files. An easier (though less optimal) approach is to post-include the maxsdk/include/3dsmax_qt_pragma_warning.h header within your wrapper header. This header takes care of conditionally suppressing compilation warnings for commonly used Qt modules.

Note:

This header depends on the existence of certain Qt preprocessor symbols, such as QT_CORE_LIB, which are defined for projects using Qt Visual Studio Tools.

Fixing C4995 Errors

You can start by suppressing those C4995 function deprecation errors that occur in system or 3rd-party headers. This is done by pre-including the affected headers before the #include 3dsmax_banned.h directive in your banned.h wrapper.

As the implementation of banned.h in the 3ds Max SDK is a continuing effort, some 3ds Max SDK headers are not yet compliant and must therefore also be pre-included if used in your plugin code (e.g. <maxscript/maxscript.h> must be pre-included because it contains the MXS_range_check macro that uses some banned functions).

Next, fix (or temporarily suppress) C4995 function deprecation errors that occur in your project header files and source files.

To view the list of "banned" functions and their safer alternatives, see:

Where possible, replace raw TCHAR* string buffers with the 3ds Max TSTR class, and use class methods like TSTR::printf() instead of low-level string manipulation functions. If the same TSTR instance is reused for several string write operations, verify that the allocated data buffer size is large enough for each write operation.
For example, some maxsdk functions like GetSpecDir() or TryCSIDLDir(), accept a TSTR& argument by reference and may reallocate (and possibly reduce) the data buffer allocation. This could affect subsequent write operations that require a larger buffer.

Some tips:

TSTR buf;
getCfgMgr().getString(INI_KEY_ARCHIVE_ARGS, buf.dataForWrite(1024), (int)buf.AllocatedChars());
// C/C++ standard does not specify order of evaluation for function arguments.
// In MSVC2015, buf.AllocatedChars() is evaluated before buf.dataForWrite(), hence buffer size of zero is passed to function

Instead, re-write your code in a way that's similar to this snippet:

getCfgMgr().getString(INI_KEY_ARCHIVE_ARGS, buf.dataForWrite(1024), 1024);
// or:
buf.Resize(1024);
getCfgMgr().getString(INI_KEY_ARCHIVE_ARGS, buf.dataForWrite(), buf.AllocatedChars());
// or better yet, modify the function to accept a TSTR& argument and have the function allocate the appropriate buffer size internally:
getCfgMgr().getString(INI_KEY_ARCHIVE_ARGS, buf);