fix: restore locale methods safely
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -2,7 +2,9 @@ User-visible changes in SES:
## Next release
-* No changes yet.
+* The `*Locale*` methods removed in the previous release are now restored
+ by aliasing them to their non-locale equivalents. `localeCompare` had no builtin
+ non-locale equivalent, so we provide one.
## Release 0.9.0 (13-July-2020)

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
@@ -26,6 +26,7 @@ import tameGlobalErrorObject from './tame-global-error-object.js';
import tameGlobalMathObject from './tame-global-math-object.js';
import tameGlobalRegExpObject from './tame-global-reg-exp-object.js';
import enablePropertyOverrides from './enable-property-overrides.js';
+import tameLocaleMethods from './tame-locale-methods.js';
let firstOptions;
@@ -54,6 +55,7 @@ export function lockdown(options = {}) {
errorTaming = 'safe',
mathTaming = 'safe',
regExpTaming = 'safe',
+ localeTaming = 'safe',
...extraOptions
} = options;
@@ -84,6 +86,7 @@ export function lockdown(options = {}) {
errorTaming,
mathTaming,
regExpTaming,
+ localeTaming,
};
/**
@@ -106,6 +109,9 @@ export function lockdown(options = {}) {
// Extract the intrinsics from the global.
const intrinsics = getIntrinsics();
+ // Replace *Locale* methods with their non-locale equivalents
+ tameLocaleMethods(intrinsics, localeTaming);
+
// Remove non-standard properties.
whitelistIntrinsics(intrinsics);

ADD
packages/ses/src/tame-locale-methods.js
+import { assert } from './assert.js';
+import { getOwnPropertyNames, defineProperty } from './commons.js';
+
+const localePattern = /^(\w*[a-z])Locale([A-Z]\w*)$/;
+
+// Use concise methods to obtain named functions without constructor
+// behavior or `.prototype` property.
+const tamedMethods = {
+ // See https://tc39.es/ecma262/#sec-string.prototype.localecompare
+ localeCompare(that) {
+ if (this === null || this === undefined) {
+ throw new TypeError(
+ `Cannot localeCompare with null or undefined "this" value`,
+ );
+ }
+ const s = `${this}`;
+ that = `${that}`;
+ if (s < that) {
+ return -1;
+ }
+ if (s > that) {
+ return 1;
+ }
+ assert(s === that, `expected ${s} and ${that} to compare`);
+ return 0;
+ },
+};
+
+const nonLocaleCompare = tamedMethods.localeCompare;
+
+export default function tameLocaleMethods(intrinsics, localeTaming = 'safe') {
+ if (localeTaming !== 'safe' && localeTaming !== 'unsafe') {
+ throw new Error(`unrecognized dateTaming ${localeTaming}`);
+ }
+ if (localeTaming === 'unsafe') {
+ return;
+ }
+
+ defineProperty(String.prototype, 'localeCompare', {
+ value: nonLocaleCompare,
+ });
+
+ for (const intrinsicName of getOwnPropertyNames(intrinsics)) {
+ const intrinsic = intrinsics[intrinsicName];
+ for (const methodName of getOwnPropertyNames(intrinsic)) {
+ const match = localePattern.exec(methodName);
+ if (match) {
+ assert(
+ typeof intrinsic[methodName] === 'function',
+ `expected ${methodName} to be a function`,
+ );
+ const nonLocaleMethodName = `${match[1]}${match[2]}`;
+ const method = intrinsic[nonLocaleMethodName];
+ assert(
+ typeof method === 'function',
+ `function ${nonLocaleMethodName} not found`,
+ );
+ defineProperty(intrinsic, methodName, { value: method });
+ }
+ }
+ }
+}

MODIFY
packages/ses/src/whitelist.js
packages/ses/src/whitelist.js
@@ -254,8 +254,8 @@ export default {
isPrototypeOf: fn,
// 19.1.3.4 Object.prototype.propertyIsEnumerable
propertyIsEnumerable: fn,
- // 19.1.3.5 Object.prototype.toLocaleString suppressed
- toLocaleString: false,
+ // 19.1.3.5 Object.prototype.toLocaleString
+ toLocaleString: fn,
// 19.1.3.6 Object.prototype.toString
toString: fn,
// 19.1.3.7 Object.prototype.valueOf
@@ -460,8 +460,8 @@ export default {
toExponential: fn,
// 20.1.3.3 Number.prototype.toFixed
toFixed: fn,
- // 20.1.3.4 Number.prototype.toLocaleString suppressed
- toLocaleString: false,
+ // 20.1.3.4 Number.prototype.toLocaleString
+ toLocaleString: fn,
// 20.1.3.5 Number.prototype.toPrecision
toPrecision: fn,
// 20.1.3.6 Number.prototype.toString
@@ -484,8 +484,8 @@ export default {
BigIntPrototype: {
// 20.2.3.1 BigInt.prototype.constructor
constructor: 'BigInt',
- // 20.2.3.2 BigInt.prototype.toLocaleString suppressed
- toLocaleString: false,
+ // 20.2.3.2 BigInt.prototype.toLocaleString
+ toLocaleString: fn,
// 20.2.3.3 BigInt.prototype.toString
toString: fn,
// 20.2.3.4 BigInt.prototype.valueOf
@@ -674,12 +674,12 @@ export default {
toISOString: fn,
// 20.4.4.37 Date.prototype.toJSON
toJSON: fn,
- // 20.4.4.38 Date.prototype.toLocaleDateString suppressed
- toLocaleDateString: false,
- // 20.4.4.39 Date.prototype.toLocaleString suppressed
- toLocaleString: false,
- // 20.4.4.40 Date.prototype.toLocaleTimeString suppressed
- toLocaleTimeString: false,
+ // 20.4.4.38 Date.prototype.toLocaleDateString
+ toLocaleDateString: fn,
+ // 20.4.4.39 Date.prototype.toLocaleString
+ toLocaleString: fn,
+ // 20.4.4.40 Date.prototype.toLocaleTimeString
+ toLocaleTimeString: fn,
// 20.4.4.41 Date.prototype.toString
toString: fn,
// 20.4.4.42 Date.prototype.toTimeString
@@ -737,8 +737,8 @@ export default {
indexOf: fn,
// 21.1.3.9 String.prototype.lastIndexOf
lastIndexOf: fn,
- // 21.1.3.10 String.prototype.localeCompare suppressed
- localeCompare: false,
+ // 21.1.3.10 String.prototype.localeCompare
+ localeCompare: fn,
// 21.1.3.11 String.prototype.match
match: fn,
// 21.1.3.12 String.prototype.matchAll
@@ -763,10 +763,10 @@ export default {
startsWith: fn,
// 21.1.3.22 String.prototype.substring
substring: fn,
- // 21.1.3.23 String.prototype.toLocaleLowerCase suppressed
- toLocaleLowerCase: false,
- // 21.1.3.24 String.prototype.toLocaleUpperCase suppressed
- toLocaleUpperCase: false,
+ // 21.1.3.23 String.prototype.toLocaleLowerCase
+ toLocaleLowerCase: fn,
+ // 21.1.3.24 String.prototype.toLocaleUpperCase
+ toLocaleUpperCase: fn,
// 21.1.3.25 String.prototype.toLowerCase
toLowerCase: fn,
// 21.1.3.26 String.prototype.
@@ -966,8 +966,8 @@ export default {
sort: fn,
// 22.1.3.28 Array.prototype.splice
splice: fn,
- // 22.1.3.29 Array.prototype.toLocaleString suppressed
- toLocaleString: false,
+ // 22.1.3.29 Array.prototype.toLocaleString
+ toLocaleString: fn,
// 22.1.3.30 Array.prototype.toString
toString: fn,
// 22.1.3.31 Array.prototype.unshift
@@ -1072,8 +1072,8 @@ export default {
sort: fn,
// 22.2.3.27 %TypedArray%.prototype.subarray
subarray: fn,
- // 22.2.3.28 %TypedArray%.prototype.toLocaleString suppressed
- toLocaleString: false,
+ // 22.2.3.28 %TypedArray%.prototype.toLocaleString
+ toLocaleString: fn,
// 22.2.3.29 %TypedArray%.prototype.toString
toString: fn,
// 22.2.3.30 %TypedArray%.prototype.values

ADD
packages/ses/test/tame-locale-methods-allow.test.js
+/* global lockdown BigInt */
+import test from 'tape';
+import '../src/main.js';
+
+lockdown();
+
+test('tame locale methods', t => {
+ t.equal(Object.prototype.toString, Object.prototype.toLocaleString);
+ t.equal(Number.prototype.toString, Number.prototype.toLocaleString);
+ t.equal(BigInt.prototype.toString, BigInt.prototype.toLocaleString);
+ t.equal(Date.prototype.toDateString, Date.prototype.toLocaleDateString);
+ t.equal(Date.prototype.toString, Date.prototype.toLocaleString);
+ t.equal(Date.prototype.toTimeString, Date.prototype.toLocaleTimeString);
+ t.equal(String.prototype.toLowerCase, String.prototype.toLocaleLowerCase);
+ t.equal(String.prototype.toUpperCase, String.prototype.toLocaleUpperCase);
+ t.equal(Array.prototype.toString, Array.prototype.toLocaleString);
+
+ const TypedArray = Reflect.getPrototypeOf(Uint8Array);
+ t.equal(TypedArray.prototype.toString, TypedArray.prototype.toLocaleString);
+
+ t.equal(typeof String.prototype.localeCompare, 'function');
+ t.not(`${String.prototype.localeCompare}`.includes('[native code]'));
+
+ t.end();
+});

ADD
packages/ses/test/tame-locale-methods.test.js
+/* global lockdown BigInt */
+import test from 'tape';
+import '../src/main.js';
+
+lockdown({ localeTaming: 'unsafe' });
+
+test('tame locale methods', t => {
+ t.notEqual(Object.prototype.toString, Object.prototype.toLocaleString);
+ t.notEqual(Number.prototype.toString, Number.prototype.toLocaleString);
+ t.notEqual(BigInt.prototype.toString, BigInt.prototype.toLocaleString);
+ t.notEqual(Date.prototype.toDateString, Date.prototype.toLocaleDateString);
+ t.notEqual(Date.prototype.toString, Date.prototype.toLocaleString);
+ t.notEqual(Date.prototype.toTimeString, Date.prototype.toLocaleTimeString);
+ t.notEqual(String.prototype.toLowerCase, String.prototype.toLocaleLowerCase);
+ t.notEqual(String.prototype.toUpperCase, String.prototype.toLocaleUpperCase);
+ t.notEqual(Array.prototype.toString, Array.prototype.toLocaleString);
+
+ const TypedArray = Reflect.getPrototypeOf(Uint8Array);
+ t.notEqual(
+ TypedArray.prototype.toString,
+ TypedArray.prototype.toLocaleString,
+ );
+
+ t.equal(typeof String.prototype.localeCompare, 'function');
+ t.ok(`${String.prototype.localeCompare}`.includes('[native code]'));
+
+ t.end();
+});

fix: restore locale methods safely (#382)