chore: restrict writing to certain object keys (#176)
- Block 'prototype', '__proto__'& 'constructor'
- Updated 'addBuildPhase' to ensure buildPhaseType does not accept restricted key.
- Updated 'addTargetAttribute' to ensure target.uuid does not accept restricted key.
- Updated 'pbxCreateGroupWithType' to ensure groupType does not accept restricted key.
- Updated tests for 'addBuildPhase' & 'addTargetAttribute'
- Added test case for 'pbxCreateGroupWithType'
- Small test update to correct spelling of build phase type 'PBXFrameworksBuildPhase'
diff --git a/lib/pbxProject.js b/lib/pbxProject.js
index c7ac423..07fc062 100644
--- a/lib/pbxProject.js
+++ b/lib/pbxProject.js
@@ -80,6 +80,15 @@
// MARK: End of Typings
+// MARK: Start of Internals
+
+/**
+ * A list of restricted object keys.
+ */
+const RESTRICTED_OBJECT_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
+
+// MARK: End of Internals
+
function PBXProject (filename) {
if (!(this instanceof PBXProject)) { return new PBXProject(filename); }
@@ -921,6 +930,13 @@
* @returns {AddBuildPhaseResults} object containing the build phase & uuid
*/
PBXProject.prototype.addBuildPhase = function (filePathsArray, buildPhaseType, comment, target, optionsOrFolderType, subfolderPath) {
+ // Xcode may introduce new build phase types so the buildPhaseType
+ // param will not be validated against a fixed set of values.
+ // Instead, it will reject object keys that shouldn't be writable.
+ if (RESTRICTED_OBJECT_KEYS.has(buildPhaseType)) {
+ throw new Error(`"${buildPhaseType}" is restricted and cannot be used as a build phase type.`);
+ }
+
const fileReferenceSection = this.pbxFileReferenceSection();
const buildFileSection = this.pbxBuildFileSection();
const buildPhaseUuid = this.generateUuid();
@@ -1918,6 +1934,11 @@
// Create comment
const commendId = key + '_comment';
+ // Reject object keys that shouldn't be writable.
+ if (RESTRICTED_OBJECT_KEYS.has(groupType)) {
+ throw new Error(`"${groupType}" is restricted and cannot be used as a group type.`);
+ }
+
// add obj and commentObj to groups;
let groups = this.hash.project.objects[groupType];
if (!groups) {
@@ -2212,6 +2233,12 @@
attributes.TargetAttributes = {};
}
target = target || this.getFirstTarget();
+
+ // Reject object keys that shouldn't be writable.
+ if (RESTRICTED_OBJECT_KEYS.has(target.uuid)) {
+ throw new Error(`"${target.uuid}" is restricted and cannot be used as a uuid.`);
+ }
+
if (attributes.TargetAttributes[target.uuid] === undefined) {
attributes.TargetAttributes[target.uuid] = {};
}
diff --git a/test/addBuildPhase.js b/test/addBuildPhase.js
index 5d1ace2..2711a43 100644
--- a/test/addBuildPhase.js
+++ b/test/addBuildPhase.js
@@ -219,4 +219,22 @@
assert.equal(buildPhase.shellScript, '"test"');
assert.strictEqual(buildPhase.alwaysOutOfDate, undefined);
});
+
+ it('should not throw error when a non-restricted value is used for build phase type.', () => {
+ assert.doesNotThrow(
+ () => {
+ proj.addBuildPhase(['MyAssets.xcassets'], 'FooBar', 'Resources');
+ },
+ /Error: "FooBar" is restricted and cannot be used as a build phase type\./
+ );
+ });
+
+ it('should throw error when a restricted value was passed in for a build phase type.', () => {
+ assert.throws(
+ () => {
+ proj.addBuildPhase(['MyAssets.xcassets'], 'prototype', 'Resources');
+ },
+ /Error: "prototype" is restricted and cannot be used as a build phase type\./
+ );
+ });
});
diff --git a/test/addTarget.js b/test/addTarget.js
index 3fbba2f..4ae0dd8 100644
--- a/test/addTarget.js
+++ b/test/addTarget.js
@@ -169,7 +169,7 @@
const resourceFile = proj.addResourceFile('assets.bundle', options);
const resourcePhase = proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid);
const frameworkFile = proj.addFramework('libsqlite3.dylib', options);
- const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworkBuildPhase', 'Frameworks', target.uuid);
+ const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid);
const headerFile = proj.addHeaderFile('file.h', options);
assert.ok(sourcePhase);
diff --git a/test/addWatch2App.js b/test/addWatch2App.js
index de15ce6..8baf046 100644
--- a/test/addWatch2App.js
+++ b/test/addWatch2App.js
@@ -84,7 +84,7 @@
const resourceFile = proj.addResourceFile('assets.bundle', options);
const resourcePhase = proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid);
const frameworkFile = proj.addFramework('libsqlite3.dylib', options);
- const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworkBuildPhase', 'Frameworks', target.uuid);
+ const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid);
const headerFile = proj.addHeaderFile('file.h', options);
assert.ok(sourcePhase);
diff --git a/test/addWatch2Extension.js b/test/addWatch2Extension.js
index f837073..61a2ce5 100644
--- a/test/addWatch2Extension.js
+++ b/test/addWatch2Extension.js
@@ -64,7 +64,7 @@
const resourceFile = proj.addResourceFile('assets.bundle', options);
const resourcePhase = proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid);
const frameworkFile = proj.addFramework('libsqlite3.dylib', options);
- const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworkBuildPhase', 'Frameworks', target.uuid);
+ const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid);
const headerFile = proj.addHeaderFile('file.h', options);
assert.ok(sourcePhase);
diff --git a/test/addWatchApp.js b/test/addWatchApp.js
index 85b31bd..47a69a9 100644
--- a/test/addWatchApp.js
+++ b/test/addWatchApp.js
@@ -84,7 +84,7 @@
const resourceFile = proj.addResourceFile('assets.bundle', options);
const resourcePhase = proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid);
const frameworkFile = proj.addFramework('libsqlite3.dylib', options);
- const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworkBuildPhase', 'Frameworks', target.uuid);
+ const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid);
const headerFile = proj.addHeaderFile('file.h', options);
assert.ok(sourcePhase);
diff --git a/test/addWatchExtension.js b/test/addWatchExtension.js
index 10c68c2..c450519 100644
--- a/test/addWatchExtension.js
+++ b/test/addWatchExtension.js
@@ -63,7 +63,7 @@
const resourceFile = proj.addResourceFile('assets.bundle', options);
const resourcePhase = proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid);
const frameworkFile = proj.addFramework('libsqlite3.dylib', options);
- const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworkBuildPhase', 'Frameworks', target.uuid);
+ const frameworkPhase = proj.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid);
const headerFile = proj.addHeaderFile('file.h', options);
assert.ok(sourcePhase);
diff --git a/test/group.js b/test/group.js
index c640ad5..0316f77 100644
--- a/test/group.js
+++ b/test/group.js
@@ -434,5 +434,23 @@
output = project.writeSync();
assert.equal(output.match(/ProvisioningStyle\s*=\s*Manual/g), null);
});
+
+ it('should not throw error when a non-restricted value is used for a target.uuid.', () => {
+ assert.doesNotThrow(
+ () => {
+ project.addTargetAttribute('ProvisioningStyle', 'Manual', { uuid: 'FooBar' });
+ },
+ /Error: "FooBar" is restricted and cannot be used as a uuid\./
+ );
+ });
+
+ it('should throw error when a restricted value was passed in for a target.uuid.', () => {
+ assert.throws(
+ () => {
+ project.addTargetAttribute('ProvisioningStyle', 'Manual', { uuid: 'prototype' });
+ },
+ /Error: "prototype" is restricted and cannot be used as a uuid\./
+ );
+ });
});
});
diff --git a/test/pbxCreateGroupWithType.js b/test/pbxCreateGroupWithType.js
new file mode 100644
index 0000000..c168ca7
--- /dev/null
+++ b/test/pbxCreateGroupWithType.js
@@ -0,0 +1,49 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+const { describe, it, beforeEach } = require('node:test');
+const assert = require('node:assert');
+
+const PBXProject = require('../lib/pbxProject');
+let project;
+
+describe('pbxCreateGroupWithType', () => {
+ beforeEach(() => {
+ project = new PBXProject('test/parser/projects/group.pbxproj');
+ project.parseSync();
+ });
+
+ it('should not throw error when a non-restricted value is used as a group type.', () => {
+ assert.doesNotThrow(
+ () => {
+ project.pbxCreateGroupWithType('name', '.', 'FooBar');
+ },
+ /Error: "FooBar" is restricted and cannot be used as a group type\./
+ );
+ });
+
+ it('should throw error when a restricted value was passed in as a group type.', () => {
+ assert.throws(
+ () => {
+ project.pbxCreateGroupWithType('name', '.', 'prototype');
+ },
+ /Error: "prototype" is restricted and cannot be used as a group type\./
+ );
+ });
+});