From 90b9da45f4084958535338d1c4d71a22d6136aab Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Mon, 15 Jun 2020 17:41:35 -0700
Subject: [PATCH] sanitize extension ID in getExtensionIdForOpcode

---
 docs/extensions.md             | 1 +
 src/serialization/sb3.js       | 8 ++++++--
 test/unit/serialization_sb3.js | 3 +++
 3 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/docs/extensions.md b/docs/extensions.md
index 350c0204f..44638107a 100644
--- a/docs/extensions.md
+++ b/docs/extensions.md
@@ -310,6 +310,7 @@ class SomeBlocks {
         return {
             // Required: the machine-readable name of this extension.
             // Will be used as the extension's namespace.
+            // Allowed characters are those matching the regular expression [\w-]: A-Z, a-z, 0-9, and hyphen ("-").
             id: 'someBlocks',
 
             // Core extensions only: override the default extension block colors.
diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js
index d49365671..bb82390a1 100644
--- a/src/serialization/sb3.js
+++ b/src/serialization/sb3.js
@@ -273,13 +273,17 @@ const compressInputTree = function (block, blocks) {
 };
 
 /**
- * Get non-core extension ID for a given sb3 opcode.
+ * Get sanitized non-core extension ID for a given sb3 opcode.
+ * Note that this should never return a URL. If in the future the SB3 loader supports loading extensions by URL, this
+ * ID should be used to (for example) look up the extension's full URL from a table in the SB3's JSON.
  * @param {!string} opcode The opcode to examine for extension.
  * @return {?string} The extension ID, if it exists and is not a core extension.
  */
 const getExtensionIdForOpcode = function (opcode) {
+    // Allowed ID characters are those matching the regular expression [\w-]: A-Z, a-z, 0-9, and hyphen ("-").
     const index = opcode.indexOf('_');
-    const prefix = opcode.substring(0, index);
+    const forbiddenSymbols = /[^\w-]/g;
+    const prefix = opcode.substring(0, index).replace(forbiddenSymbols, '-');
     if (CORE_EXTENSIONS.indexOf(prefix) === -1) {
         if (prefix !== '') return prefix;
     }
diff --git a/test/unit/serialization_sb3.js b/test/unit/serialization_sb3.js
index 5ef1ac80d..baeb23543 100644
--- a/test/unit/serialization_sb3.js
+++ b/test/unit/serialization_sb3.js
@@ -290,6 +290,9 @@ test('getExtensionIdForOpcode', t => {
     // does not return anything for opcodes with no extension
     t.false(sb3.getExtensionIdForOpcode('hello'));
 
+    // forbidden characters must be replaced with '-'
+    t.equal(sb3.getExtensionIdForOpcode('hi:there/happy_people'), 'hi-there-happy');
+
     t.end();
 });