Skip to content

Commit 8db062a

Browse files
authored
Fix security issues without breaking local loading (#4958)
* Fix security issues without breaking local loading * deleted overides * resolved eslint error * fixed prettier issue
1 parent bbeb6fe commit 8db062a

File tree

4 files changed

+123
-61
lines changed

4 files changed

+123
-61
lines changed

electron-main.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ function createWindow() {
1010
title: "Music Blocks",
1111
webPreferences: {
1212
nodeIntegration: false,
13-
contextIsolation: true
13+
contextIsolation: true,
14+
sandbox: true,
15+
webSecurity: true,
16+
enableRemoteModule: false
1417
}
1518
});
1619

js/activity.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7098,7 +7098,10 @@ class Activity {
70987098
// Load any plugins saved in local storage.
70997099
this.pluginData = this.storage.plugins;
71007100
if (this.pluginData !== null && this.pluginData !== "null") {
7101-
updatePluginObj(this, processPluginData(this, this.pluginData));
7101+
updatePluginObj(
7102+
this,
7103+
processPluginData(this, this.pluginData, "localStorage:plugins")
7104+
);
71027105
}
71037106

71047107
// Load custom mode saved in local storage.
@@ -7399,6 +7402,7 @@ class Activity {
73997402

74007403
// Read file here.
74017404
const reader = new FileReader();
7405+
const pluginFile = that.pluginChooser.files[0];
74027406

74037407
// eslint-disable-next-line no-unused-vars
74047408
reader.onload = theFile => {
@@ -7407,7 +7411,11 @@ class Activity {
74077411
//doLoadAnimation();
74087412

74097413
setTimeout(() => {
7410-
const obj = processRawPluginData(that, reader.result);
7414+
const obj = processRawPluginData(
7415+
that,
7416+
reader.result,
7417+
pluginFile && pluginFile.name ? pluginFile.name : "local-file"
7418+
);
74117419
// Save plugins to local storage.
74127420
if (obj !== null) {
74137421
that.storage.plugins = preparePluginExports(that, obj);
@@ -7425,7 +7433,7 @@ class Activity {
74257433
}, 200);
74267434
};
74277435

7428-
reader.readAsText(that.pluginChooser.files[0]);
7436+
reader.readAsText(pluginFile);
74297437
},
74307438
false
74317439
);

js/utils/utils.js

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -575,11 +575,49 @@ let toTitleCase = str => {
575575
* @param {string} pluginData - The JSON-encoded plugin data.
576576
* @returns {object|null} The processed plugin data object or null if parsing fails.
577577
*/
578-
const processPluginData = (activity, pluginData) => {
578+
const processPluginData = (activity, pluginData, pluginSource) => {
579579
// Plugins are JSON-encoded dictionaries.
580580
if (pluginData === undefined) {
581581
return null;
582582
}
583+
584+
const isTrustedPluginSource = src => {
585+
if (!src) return false;
586+
587+
// allow only local paths
588+
return (
589+
src.startsWith("./") ||
590+
src.startsWith("../") ||
591+
src.startsWith("/") ||
592+
(!src.includes("http://") && !src.includes("https://"))
593+
);
594+
};
595+
596+
const safeEval = (code, label = "plugin") => {
597+
if (typeof code !== "string") return;
598+
599+
// basic sanity limit (prevents huge payloads)
600+
if (code.length > 500000) {
601+
// eslint-disable-next-line no-console
602+
console.warn("Plugin code too large:", label);
603+
return;
604+
}
605+
606+
try {
607+
// eslint-disable-next-line no-eval
608+
eval(code);
609+
} catch (e) {
610+
// eslint-disable-next-line no-console
611+
console.error("Plugin execution failed:", label, e);
612+
}
613+
};
614+
615+
if (!isTrustedPluginSource(pluginSource)) {
616+
// eslint-disable-next-line no-console
617+
console.warn("Blocked untrusted plugin source:", pluginSource);
618+
return null;
619+
}
620+
583621
let obj;
584622
try {
585623
obj = JSON.parse(pluginData);
@@ -719,18 +757,13 @@ const processPluginData = (activity, pluginData) => {
719757
for (const block in obj["BLOCKPLUGINS"]) {
720758
// eslint-disable-next-line no-console
721759
console.debug("adding plugin block " + block);
722-
try {
723-
eval(obj["BLOCKPLUGINS"][block]);
724-
} catch (e) {
725-
// eslint-disable-next-line no-console
726-
console.debug("Failed to load plugin for " + block + ": " + e);
727-
}
760+
safeEval(obj["BLOCKPLUGINS"][block], "BLOCKPLUGINS:" + block);
728761
}
729762
}
730763

731764
// Create the globals.
732765
if ("GLOBALS" in obj) {
733-
eval(obj["GLOBALS"]);
766+
safeEval(obj["GLOBALS"], "GLOBALS");
734767
}
735768

736769
if ("PARAMETERPLUGINS" in obj) {
@@ -742,7 +775,7 @@ const processPluginData = (activity, pluginData) => {
742775
// Code to execute when plugin is loaded
743776
if ("ONLOAD" in obj) {
744777
for (const arg in obj["ONLOAD"]) {
745-
eval(obj["ONLOAD"][arg]);
778+
safeEval(obj["ONLOAD"][arg], "ONLOAD:" + arg);
746779
}
747780
}
748781

@@ -799,7 +832,7 @@ const processPluginData = (activity, pluginData) => {
799832
* @param {string} rawData - Raw plugin data to process.
800833
* @returns {object|null} The processed plugin data object or null if parsing fails.
801834
*/
802-
const processRawPluginData = (activity, rawData) => {
835+
const processRawPluginData = (activity, rawData, pluginSource) => {
803836
const lineData = rawData.split("\n");
804837
let cleanData = "";
805838

@@ -821,7 +854,7 @@ const processRawPluginData = (activity, rawData) => {
821854
// try/catch while debugging your plugin.
822855
let obj;
823856
try {
824-
obj = processPluginData(activity, cleanData.replace(/\n/g, ""));
857+
obj = processPluginData(activity, cleanData.replace(/\n/g, ""), pluginSource);
825858
} catch (e) {
826859
obj = null;
827860
// eslint-disable-next-line no-console

planet/js/ProjectViewer.js

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121
*/
2222

2323
class ProjectViewer {
24-
2524
constructor(Planet) {
26-
this.Planet = Planet ;
25+
this.Planet = Planet;
2726
this.ProjectCache = Planet.GlobalPlanet.cache;
2827
this.PlaceholderMBImage = "images/mbgraphic.png";
2928
this.PlaceholderTBImage = "images/tbgraphic.png";
3029
this.ReportError = _("Error: Report could not be submitted. Try again later.");
31-
this.ReportSuccess = _("Thank you for reporting this project. A moderator will review the project shortly, to verify violation of the Sugar Labs Code of Conduct.");
30+
this.ReportSuccess = _(
31+
"Thank you for reporting this project. A moderator will review the project shortly, to verify violation of the Sugar Labs Code of Conduct."
32+
);
3233
this.ReportEnabledButton = _("Report Project");
3334
this.ReportDisabledButton = _("Project Reported");
3435
this.ReportDescriptionError = _("Report description required");
@@ -37,25 +38,45 @@ class ProjectViewer {
3738
}
3839

3940
open(id) {
40-
const Planet = this.Planet ;
41+
const Planet = this.Planet;
4142
this.id = id;
4243
const proj = this.ProjectCache[id];
43-
const options = { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "numeric" };
44+
45+
const setBoldText = (el, text) => {
46+
el.textContent = "";
47+
const b = document.createElement("b");
48+
b.textContent = String(text);
49+
el.appendChild(b);
50+
};
51+
52+
const options = {
53+
year: "numeric",
54+
month: "short",
55+
day: "numeric",
56+
hour: "numeric",
57+
minute: "numeric"
58+
};
4459
const last_updated_timestamp = proj.ProjectLastUpdated;
45-
const formatted_LastUpdated = new Date(last_updated_timestamp).toLocaleString(undefined, options);
60+
const formatted_LastUpdated = new Date(last_updated_timestamp).toLocaleString(
61+
undefined,
62+
options
63+
);
4664
const created_timestamp = proj.ProjectCreatedDate;
47-
const formatted_CreatedDate = new Date(created_timestamp).toLocaleString(undefined, options);
65+
const formatted_CreatedDate = new Date(created_timestamp).toLocaleString(
66+
undefined,
67+
options
68+
);
4869

4970
document.getElementById("projectviewer-title").textContent = proj.ProjectName;
50-
document.getElementById("projectviewer-last-updated").innerHTML = `<b>${formatted_LastUpdated}</b>`;
51-
document.getElementById("projectviewer-date").innerHTML =`<b>${formatted_CreatedDate}</b>`;
52-
document.getElementById("projectviewer-downloads").innerHTML = `<b>${proj.ProjectDownloads}</b>`;
53-
document.getElementById("projectviewer-likes").innerHTML = `<b>${proj.ProjectLikes}</b>`;
71+
setBoldText(document.getElementById("projectviewer-last-updated"), formatted_LastUpdated);
72+
setBoldText(document.getElementById("projectviewer-date"), formatted_CreatedDate);
73+
setBoldText(document.getElementById("projectviewer-downloads"), proj.ProjectDownloads);
74+
setBoldText(document.getElementById("projectviewer-likes"), proj.ProjectLikes);
5475

5576
let img = proj.ProjectImage;
5677
if (img === "" || img === null)
57-
img = (proj.ProjectIsMusicBlocks==1) ?
58-
this.PlaceholderMBImage : this.PlaceholderTBImage ;
78+
img =
79+
proj.ProjectIsMusicBlocks == 1 ? this.PlaceholderMBImage : this.PlaceholderTBImage;
5980

6081
document.getElementById("projectviewer-image").src = img;
6182
document.getElementById("projectviewer-description").textContent = proj.ProjectDescription;
@@ -68,29 +89,28 @@ class ProjectViewer {
6889
tagcontainer.appendChild(chip);
6990
}
7091

71-
if (Planet.ProjectStorage.isReported(this.id)){
92+
if (Planet.ProjectStorage.isReported(this.id)) {
7293
document.getElementById("projectviewer-report-project").style.display = "none";
73-
document.getElementById("projectviewer-report-project-disabled").style.display = "block";
74-
}
75-
else {
94+
document.getElementById("projectviewer-report-project-disabled").style.display =
95+
"block";
96+
} else {
7697
document.getElementById("projectviewer-report-project").style.display = "block";
7798
document.getElementById("projectviewer-report-project-disabled").style.display = "none";
7899
}
79100

80101
jQuery("#projectviewer").modal("open");
81-
};
102+
}
82103

83104
download() {
84-
this.Planet.GlobalPlanet.getData(this.id,this.afterDownload.bind(this));
85-
};
105+
this.Planet.GlobalPlanet.getData(this.id, this.afterDownload.bind(this));
106+
}
86107

87-
afterDownload (data) {
88-
const Planet = this.Planet ;
108+
afterDownload(data) {
109+
const Planet = this.Planet;
89110

90111
const proj = this.ProjectCache[this.id];
91112
let image = Planet.ProjectStorage.ImageDataURL;
92-
if (proj.ProjectImage !== "")
93-
image = proj.ProjectImage;
113+
if (proj.ProjectImage !== "") image = proj.ProjectImage;
94114

95115
Planet.SaveInterface.saveHTML(
96116
proj.ProjectName,
@@ -99,19 +119,19 @@ class ProjectViewer {
99119
proj.ProjectDescription,
100120
this.id
101121
);
102-
};
122+
}
103123

104124
openProject() {
105125
// newPageTitle = proj.ProjectName;
106126
// document.title = newPageTitle;
107127
this.Planet.GlobalPlanet.openGlobalProject(this.id);
108-
};
128+
}
109129

110130
mergeProject() {
111131
// newPageTitle = proj.ProjectName;
112132
// document.title = newPageTitle;
113133
this.Planet.GlobalPlanet.mergeGlobalProject(this.id);
114-
};
134+
}
115135

116136
openReporter() {
117137
// eslint-disable-next-line no-console
@@ -121,55 +141,54 @@ class ProjectViewer {
121141
document.getElementById("projectviewer-report-progress").style.visibility = "hidden";
122142
document.getElementById("report-error").style.display = "none";
123143
document.getElementById("projectviewer-report-card").style.display = "block";
124-
144+
125145
hideOnClickOutside(
126146
[
127147
document.getElementById("projectviewer-report-card"),
128148
document.getElementById("projectviewer-report-project")
129149
],
130150
"projectviewer-report-card"
131151
);
132-
};
152+
}
133153

134154
submitReporter() {
135155
const text = document.getElementById("reportdescription").value;
136156

137-
if (text === ""){
157+
if (text === "") {
138158
document.getElementById("report-error").textContent = this.ReportDescriptionError;
139159
document.getElementById("report-error").style.display = "block";
140160
return;
141-
}
142-
else if (text.length > 1000){
143-
document.getElementById("report-error").textContent = this.ReportDescriptionTooLongError;
161+
} else if (text.length > 1000) {
162+
document.getElementById(
163+
"report-error"
164+
).textContent = this.ReportDescriptionTooLongError;
144165
document.getElementById("report-error").style.display = "block";
145166
return;
146-
}
147-
else {
167+
} else {
148168
document.getElementById("projectviewer-report-progress").style.visibility = "hidden";
149169
this.Planet.ServerInterface.reportProject(this.id, text, this.afterReport.bind(this));
150170
}
151-
};
171+
}
152172

153173
afterReport(data) {
154174
if (data.success) {
155175
document.getElementById("submittext").textContent = this.ReportSuccess;
156-
this.Planet.ProjectStorage.report(this.id,true);
176+
this.Planet.ProjectStorage.report(this.id, true);
157177
document.getElementById("projectviewer-report-project").style.display = "none";
158-
document.getElementById("projectviewer-report-project-disabled").style.display = "block";
159-
}
160-
else document.getElementById("submittext").textContent = this.ReportError;
178+
document.getElementById("projectviewer-report-project-disabled").style.display =
179+
"block";
180+
} else document.getElementById("submittext").textContent = this.ReportError;
161181

162182
document.getElementById("projectviewer-report-content").style.display = "none";
163183
document.getElementById("projectviewer-report-progress").style.visibility = "hidden";
164184
document.getElementById("projectviewer-reportsubmit-content").style.display = "block";
165-
};
185+
}
166186

167187
closeReporter() {
168188
document.getElementById("projectviewer-report-card").style.display = "none";
169-
};
189+
}
170190

171191
init() {
172-
173192
// eslint-disable-next-line no-unused-vars
174193
document.getElementById("projectviewer-download-file").addEventListener("click", evt => {
175194
this.download();
@@ -199,6 +218,5 @@ class ProjectViewer {
199218
document.getElementById("projectviewer-report-close").addEventListener("click", evt => {
200219
this.closeReporter();
201220
});
202-
};
203-
204-
}
221+
}
222+
}

0 commit comments

Comments
 (0)