CVE-2022-3008: Improper Neutralization of Special Elements used in a Command ('Command Injection') (CWE-77)
value: The tinygltf library uses the C library function wordexp() to perform file path expansion on untrusted paths that are provided from the input file. This function allows for command injection by using backticks. An attacker could craft an untrusted path input that would result in a path expansion. We recommend upgrading to 2.6.0 or past commit 52ff00a38447f06a17eab1caa2cf0730a119c751
CWE description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
CVSS v3 base score: 8.8
Published: 2022-09-05T09:15Z
Repository: https://github.com/syoyo/tinygltf
Language: C
Files changed: tiny_gltf.h
Commit date: 2022-08-16 20:08:45 +0900
Commit message: Do not expand file path since its not necessary for glTF asset path(URI) and for security reason(`wordexp`).

Diff: diff --git a/tiny_gltf.h b/tiny_gltf.h
index 59756e9..7481345 100644
--- a/tiny_gltf.h
+++ b/tiny_gltf.h
@@ -24,10 +24,11 @@
 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
 // Version:
+//  - v2.6.0 Disable expanding file path for security(no use of awkward `wordexp` anymore).
 //  - v2.5.0 Add SetPreserveImageChannels() option to load image data as is.
 //  - v2.4.3 Fix null object output when when material has all default
 //  parameters.
 //  - v2.4.2 Decode percent-encoded URI.
 //  - v2.4.1 Fix some glTF object class does not have `extensions` and/or
@@ -106,11 +107,15 @@ namespace tinygltf {
 #define TINYGLTF_COMPONENT_TYPE_SHORT (5122)
 #define TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT (5123)
 #define TINYGLTF_COMPONENT_TYPE_INT (5124)
 #define TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT (5125)
 #define TINYGLTF_COMPONENT_TYPE_FLOAT (5126)
-#define TINYGLTF_COMPONENT_TYPE_DOUBLE (5130) // OpenGL double type. Note that some of glTF 2.0 validator does not support double type even the schema seems allow any value of integer: https://github.com/KhronosGroup/glTF/blob/b9884a2fd45130b4d673dd6c8a706ee21ee5c5f7/specification/2.0/schema/accessor.schema.json#L22
+#define TINYGLTF_COMPONENT_TYPE_DOUBLE \
+  (5130)  // OpenGL double type. Note that some of glTF 2.0 validator does not
+          // support double type even the schema seems allow any value of
+          // integer:
+          // https://github.com/KhronosGroup/glTF/blob/b9884a2fd45130b4d673dd6c8a706ee21ee5c5f7/specification/2.0/schema/accessor.schema.json#L22
 
 #define TINYGLTF_TEXTURE_FILTER_NEAREST (9728)
 #define TINYGLTF_TEXTURE_FILTER_LINEAR (9729)
 #define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST (9984)
 #define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST (9985)
@@ -611,11 +616,12 @@ struct Sampler {
       TINYGLTF_TEXTURE_WRAP_REPEAT;  // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT",
                                      // "REPEAT"], default "REPEAT"
   int wrapT =
       TINYGLTF_TEXTURE_WRAP_REPEAT;  // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT",
                                      // "REPEAT"], default "REPEAT"
-  //int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;  // TinyGLTF extension. currently not used.
+  // int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;  // TinyGLTF extension. currently
+  // not used.
 
   Value extras;
   ExtensionMap extensions;
 
   // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
@@ -1300,12 +1306,14 @@ class TinyGLTF {
                          unsigned int check_sections = REQUIRE_VERSION);
 
   ///
   /// Loads glTF ASCII asset from string(memory).
   /// `length` = strlen(str);
-  /// Set warning message to `warn` for example it fails to load asserts.
-  /// Returns false and set error string to `err` if there's an error.
+  /// `base_dir` is a search path of glTF asset(e.g. images). Path Must be an
+  /// expanded path (e.g. no tilde(`~`), no environment variables). Set warning
+  /// message to `warn` for example it fails to load asserts. Returns false and
+  /// set error string to `err` if there's an error.
   ///
   bool LoadASCIIFromString(Model *model, std::string *err, std::string *warn,
                            const char *str, const unsigned int length,
                            const std::string &base_dir,
                            unsigned int check_sections = REQUIRE_VERSION);
@@ -1320,10 +1328,12 @@ class TinyGLTF {
                           unsigned int check_sections = REQUIRE_VERSION);
 
   ///
   /// Loads glTF binary asset from memory.
   /// `length` = strlen(str);
+  /// `base_dir` is a search path of glTF asset(e.g. images). Path Must be an
+  /// expanded path (e.g. no tilde(`~`), no environment variables).
   /// Set warning message to `warn` for example it fails to load asserts.
   /// Returns false and set error string to `err` if there's an error.
   ///
   bool LoadBinaryFromMemory(Model *model, std::string *err, std::string *warn,
                             const unsigned char *bytes,
@@ -1603,11 +1613,11 @@ class TinyGLTF {
 #include <ext/stdio_filebuf.h>  // fstream (all sorts of IO stuff) + stdio_filebuf (=streambuf)
 
 #endif
 
 #elif !defined(__ANDROID__) && !defined(__OpenBSD__)
-#include <wordexp.h>
+//#include <wordexp.h>
 #endif
 
 #if defined(__sparcv9) || defined(__powerpc__)
 // Big endian
 #else
@@ -1931,14 +1941,13 @@ bool Primitive::operator==(const Primitive &other) const {
 }
 bool Sampler::operator==(const Sampler &other) const {
   return this->extensions == other.extensions && this->extras == other.extras &&
          this->magFilter == other.magFilter &&
          this->minFilter == other.minFilter && this->name == other.name &&
-         this->wrapS == other.wrapS &&
-         this->wrapT == other.wrapT;
+         this->wrapS == other.wrapS && this->wrapT == other.wrapT;
 
-         //this->wrapR == other.wrapR
+  // this->wrapR == other.wrapR
 }
 bool Scene::operator==(const Scene &other) const {
   return this->extensions == other.extensions && this->extras == other.extras &&
          this->name == other.name && this->nodes == other.nodes;
 }
@@ -2040,12 +2049,11 @@ static std::string GetBaseDir(const std::string &filepath) {
   return "";
 }
 
 static std::string GetBaseFilename(const std::string &filepath) {
   auto idx = filepath.find_last_of("/\\");
-  if (idx != std::string::npos)
-    return filepath.substr(idx + 1);
+  if (idx != std::string::npos) return filepath.substr(idx + 1);
   return filepath;
 }
 
 std::string base64_encode(unsigned char const *, unsigned int len);
 std::string base64_decode(std::string const &s);
@@ -2603,10 +2611,22 @@ bool FileExists(const std::string &abs_filename, void *) {
 
   return ret;
 }
 
 std::string ExpandFilePath(const std::string &filepath, void *) {
+  // https://github.com/syoyo/tinygltf/issues/368
+  //
+  // No file path expansion in built-in FS function anymore, since glTF URI
+  // should not contain tilde('~') and environment variables, and for security
+  // reason(`wordexp`).
+  //
+  // Users need to supply `base_dir`(in `LoadASCIIFromString`,
+  // `LoadBinaryFromMemory`) in expanded absolute path.
+
+  return filepath;
+
+#if 0
 #ifdef _WIN32
   // Assume input `filepath` is encoded in UTF-8
   std::wstring wfilepath = UTF8ToWchar(filepath);
   DWORD wlen = ExpandEnvironmentStringsW(wfilepath.c_str(), nullptr, 0);
   wchar_t *wstr = new wchar_t[wlen];
@@ -2650,10 +2670,11 @@ std::string ExpandFilePath(const std::string &filepath, void *) {
 
 #endif
 
   return s;
 #endif
+#endif
 }
 
 bool ReadWholeFile(std::vector<unsigned char> *out, std::string *err,
                    const std::string &filepath, void *) {
 #ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS
@@ -4240,24 +4261,24 @@ static bool ParseSparseAccessor(Accessor *accessor, std::string *err,
 
   const json &indices_obj = GetValue(indices_iterator);
   const json &values_obj = GetValue(values_iterator);
 
   int indices_buffer_view = 0, indices_byte_offset = 0, component_type = 0;
-  if (!ParseIntegerProperty(&indices_buffer_view, err, indices_obj, "bufferView",
-                       true, "SparseAccessor")) {
+  if (!ParseIntegerProperty(&indices_buffer_view, err, indices_obj,
+                            "bufferView", true, "SparseAccessor")) {
     return false;
   }
   ParseIntegerProperty(&indices_byte_offset, err, indices_obj, "byteOffset",
                        false);
   if (!ParseIntegerProperty(&component_type, err, indices_obj, "componentType",
-                       true, "SparseAccessor")) {
+                            true, "SparseAccessor")) {
     return false;
   }
 
   int values_buffer_view = 0, values_byte_offset = 0;
   if (!ParseIntegerProperty(&values_buffer_view, err, values_obj, "bufferView",
-                       true, "SparseAccessor")) {
+                            true, "SparseAccessor")) {
     return false;
   }
   ParseIntegerProperty(&values_byte_offset, err, values_obj, "byteOffset",
                        false);
 
@@ -5092,25 +5113,26 @@ static bool ParseSampler(Sampler *sampler, std::string *err, const json &o,
 
   int minFilter = -1;
   int magFilter = -1;
   int wrapS = TINYGLTF_TEXTURE_WRAP_REPEAT;
   int wrapT = TINYGLTF_TEXTURE_WRAP_REPEAT;
-  //int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;
+  // int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;
   ParseIntegerProperty(&minFilter, err, o, "minFilter", false);
   ParseIntegerProperty(&magFilter, err, o, "magFilter", false);
   ParseIntegerProperty(&wrapS, err, o, "wrapS", false);
   ParseIntegerProperty(&wrapT, err, o, "wrapT", false);
-  //ParseIntegerProperty(&wrapR, err, o, "wrapR", false);  // tinygltf extension
+  // ParseIntegerProperty(&wrapR, err, o, "wrapR", false);  // tinygltf
+  // extension
 
   // TODO(syoyo): Check the value is alloed one.
   // (e.g. we allow 9728(NEAREST), but don't allow 9727)
 
   sampler->minFilter = minFilter;
   sampler->magFilter = magFilter;
   sampler->wrapS = wrapS;
   sampler->wrapT = wrapT;
-  //sampler->wrapR = wrapR;
+  // sampler->wrapR = wrapR;
 
   ParseExtensionsProperty(&(sampler->extensions), err, o);
   ParseExtrasProperty(&(sampler->extras), o);
 
   if (store_original_json_for_extras_and_extensions) {
@@ -7169,11 +7191,11 @@ static void SerializeGltfSampler(Sampler &sampler, json &o) {
     SerializeNumberProperty("magFilter", sampler.magFilter, o);
   }
   if (sampler.minFilter != -1) {
     SerializeNumberProperty("minFilter", sampler.minFilter, o);
   }
-  //SerializeNumberProperty("wrapR", sampler.wrapR, o);
+  // SerializeNumberProperty("wrapR", sampler.wrapR, o);
   SerializeNumberProperty("wrapS", sampler.wrapS, o);
   SerializeNumberProperty("wrapT", sampler.wrapT, o);
 
   if (sampler.extras.Type() != NULL_TYPE) {
     SerializeValue("extras", sampler.extras, o);
@@ -7532,12 +7554,14 @@ static void WriteBinaryGltfStream(std::ostream &stream,
   const int version = 2;
 
   const uint32_t content_size = uint32_t(content.size());
   const uint32_t binBuffer_size = uint32_t(binBuffer.size());
   // determine number of padding bytes required to ensure 4 byte alignment
-  const uint32_t content_padding_size = content_size % 4 == 0 ? 0 : 4 - content_size % 4;
-  const uint32_t bin_padding_size = binBuffer_size % 4 == 0 ? 0 : 4 - binBuffer_size % 4;
+  const uint32_t content_padding_size =
+      content_size % 4 == 0 ? 0 : 4 - content_size % 4;
+  const uint32_t bin_padding_size =
+      binBuffer_size % 4 == 0 ? 0 : 4 - binBuffer_size % 4;
 
   // 12 bytes for header, JSON content length, 8 bytes for JSON chunk info.
   // Chunk data must be located at 4-byte boundary, which may require padding
   const uint32_t length =
       12 + 8 + content_size + content_padding_size +

Vulnerable code: #define TINYGLTF_COMPONENT_TYPE_DOUBLE (5130) // OpenGL double type. Note that some of glTF 2.0 validator does not support double type even the schema seems allow any value of integer: https://github.com/KhronosGroup/glTF/blob/b9884a2fd45130b4d673dd6c8a706ee21ee5c5f7/specification/2.0/schema/accessor.schema.json#L22
  //int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;  // TinyGLTF extension. currently not used.
  /// Set warning message to `warn` for example it fails to load asserts.
  /// Returns false and set error string to `err` if there's an error.
#include <wordexp.h>
         this->wrapS == other.wrapS &&
         this->wrapT == other.wrapT;
         //this->wrapR == other.wrapR
  if (idx != std::string::npos)
    return filepath.substr(idx + 1);
  if (!ParseIntegerProperty(&indices_buffer_view, err, indices_obj, "bufferView",
                       true, "SparseAccessor")) {
                       true, "SparseAccessor")) {
                       true, "SparseAccessor")) {
  //int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;
  //ParseIntegerProperty(&wrapR, err, o, "wrapR", false);  // tinygltf extension
  //sampler->wrapR = wrapR;
  //SerializeNumberProperty("wrapR", sampler.wrapR, o);
  const uint32_t content_padding_size = content_size % 4 == 0 ? 0 : 4 - content_size % 4;
  const uint32_t bin_padding_size = binBuffer_size % 4 == 0 ? 0 : 4 - binBuffer_size % 4;
Fixed code: //  - v2.6.0 Disable expanding file path for security(no use of awkward `wordexp` anymore).
#define TINYGLTF_COMPONENT_TYPE_DOUBLE \
  (5130)  // OpenGL double type. Note that some of glTF 2.0 validator does not
          // support double type even the schema seems allow any value of
          // integer:
          // https://github.com/KhronosGroup/glTF/blob/b9884a2fd45130b4d673dd6c8a706ee21ee5c5f7/specification/2.0/schema/accessor.schema.json#L22
  // int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;  // TinyGLTF extension. currently
  // not used.
  /// `base_dir` is a search path of glTF asset(e.g. images). Path Must be an
  /// expanded path (e.g. no tilde(`~`), no environment variables). Set warning
  /// message to `warn` for example it fails to load asserts. Returns false and
  /// set error string to `err` if there's an error.
  /// `base_dir` is a search path of glTF asset(e.g. images). Path Must be an
  /// expanded path (e.g. no tilde(`~`), no environment variables).
//#include <wordexp.h>
         this->wrapS == other.wrapS && this->wrapT == other.wrapT;
  // this->wrapR == other.wrapR
  if (idx != std::string::npos) return filepath.substr(idx + 1);
  // https://github.com/syoyo/tinygltf/issues/368
  //
  // No file path expansion in built-in FS function anymore, since glTF URI
  // should not contain tilde('~') and environment variables, and for security
  // reason(`wordexp`).
  //
  // Users need to supply `base_dir`(in `LoadASCIIFromString`,
  // `LoadBinaryFromMemory`) in expanded absolute path.

  return filepath;

#if 0
#endif
  if (!ParseIntegerProperty(&indices_buffer_view, err, indices_obj,
                            "bufferView", true, "SparseAccessor")) {
                            true, "SparseAccessor")) {
                            true, "SparseAccessor")) {
  // int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT;
  // ParseIntegerProperty(&wrapR, err, o, "wrapR", false);  // tinygltf
  // extension
  // sampler->wrapR = wrapR;
  // SerializeNumberProperty("wrapR", sampler.wrapR, o);
  const uint32_t content_padding_size =
      content_size % 4 == 0 ? 0 : 4 - content_size % 4;
  const uint32_t bin_padding_size =
      binBuffer_size % 4 == 0 ? 0 : 4 - binBuffer_size % 4;