Dealing with untrusted content - resolves #456

This commit is contained in:
David Teller 2023-01-12 14:17:53 +01:00
parent 9693149e1e
commit 13459570a1
7 changed files with 426 additions and 8 deletions

View File

@ -39,6 +39,7 @@ import { RoomMemberManager } from "./RoomMembers";
import ProtectedRoomsConfig from "./ProtectedRoomsConfig"; import ProtectedRoomsConfig from "./ProtectedRoomsConfig";
import { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter"; import { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter";
import { OpenMetrics } from "./webapis/OpenMetrics"; import { OpenMetrics } from "./webapis/OpenMetrics";
import * as UntrustedContent from "./UntrustedContent";
export const STATE_NOT_STARTED = "not_started"; export const STATE_NOT_STARTED = "not_started";
export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@ -50,6 +51,9 @@ export const STATE_RUNNING = "running";
* to store that for pagination on further polls * to store that for pagination on further polls
*/ */
export const REPORT_POLL_EVENT_TYPE = "org.matrix.mjolnir.report_poll"; export const REPORT_POLL_EVENT_TYPE = "org.matrix.mjolnir.report_poll";
const REPORT_POLL_EXPECTED_CONTENT = new UntrustedContent.SubTypeObjectContent({
"from": UntrustedContent.NUMBER_CONTENT
});
export class Mjolnir { export class Mjolnir {
private displayName: string; private displayName: string;
@ -279,6 +283,12 @@ export class Mjolnir {
let reportPollSetting: { from: number } = { from: 0 }; let reportPollSetting: { from: number } = { from: 0 };
try { try {
reportPollSetting = await this.client.getAccountData(REPORT_POLL_EVENT_TYPE); reportPollSetting = await this.client.getAccountData(REPORT_POLL_EVENT_TYPE);
reportPollSetting = REPORT_POLL_EXPECTED_CONTENT.fallback(reportPollSetting,
() => {
this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "invalid report poll settings, ignoring");
return ({ from: 0 })
}
);
} catch (err) { } catch (err) {
if (err.body?.errcode !== "M_NOT_FOUND") { if (err.body?.errcode !== "M_NOT_FOUND") {
throw err; throw err;

View File

@ -18,7 +18,12 @@ import AwaitLock from 'await-lock';
import { extractRequestError, LogService, Permalinks } from "matrix-bot-sdk"; import { extractRequestError, LogService, Permalinks } from "matrix-bot-sdk";
import { IConfig } from "./config"; import { IConfig } from "./config";
import { MatrixSendClient } from './MatrixEmitter'; import { MatrixSendClient } from './MatrixEmitter';
import * as UntrustedContent from './UntrustedContent';
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms"; const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
const PROTECTED_ROOMS_EXPECTED_CONTENT = new UntrustedContent.SubTypeObjectContent({
rooms: UntrustedContent.STRING_CONTENT.array().optional()
});
/** /**
* Manages the set of rooms that the user has EXPLICITLY asked to be protected. * Manages the set of rooms that the user has EXPLICITLY asked to be protected.
@ -65,7 +70,12 @@ export default class ProtectedRoomsConfig {
public async loadProtectedRoomsFromAccountData(): Promise<void> { public async loadProtectedRoomsFromAccountData(): Promise<void> {
LogService.debug("ProtectedRoomsConfig", "Loading protected rooms..."); LogService.debug("ProtectedRoomsConfig", "Loading protected rooms...");
try { try {
const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE); let data: { rooms?: string[] } | null = PROTECTED_ROOMS_EXPECTED_CONTENT.fallback(
await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE),
() => {
LogService.warn("ProtectedRoomsConfig", "Invalid data, assuming empty data");
return null;
});
if (data && data['rooms']) { if (data && data['rooms']) {
for (const roomId of data['rooms']) { for (const roomId of data['rooms']) {
this.explicitlyProtectedRooms.add(roomId); this.explicitlyProtectedRooms.add(roomId);
@ -116,10 +126,19 @@ export default class ProtectedRoomsConfig {
// but it doesn't stop a third party client on the same account racing with us instead. // but it doesn't stop a third party client on the same account racing with us instead.
await this.accountDataLock.acquireAsync(); await this.accountDataLock.acquireAsync();
try { try {
const additionalProtectedRooms: string[] = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE) const untrustedAdditionalProtectedRooms = await
.then((rooms: {rooms?: string[]}) => Array.isArray(rooms?.rooms) ? rooms.rooms : []) this
.catch(e => (LogService.warn("ProtectedRoomsConfig", "Could not load protected rooms from account data", extractRequestError(e)), [])); .client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE)
.catch(e => {
LogService.warn("ProtectedRoomsConfig", "Could not load protected rooms from account data", extractRequestError(e));
return [];
});
let additionalProtectedRooms = PROTECTED_ROOMS_EXPECTED_CONTENT.fallback(untrustedAdditionalProtectedRooms,
() => {
LogService.warn("ProtectedRoomsConfig", "Invalid list of protected rooms, restarting with an empty list");
return [];
}
);
const roomsToSave = new Set([...this.explicitlyProtectedRooms.keys(), ...additionalProtectedRooms]); const roomsToSave = new Set([...this.explicitlyProtectedRooms.keys(), ...additionalProtectedRooms]);
excludeRooms.forEach(roomsToSave.delete, roomsToSave); excludeRooms.forEach(roomsToSave.delete, roomsToSave);
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: Array.from(roomsToSave.keys()) }); await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: Array.from(roomsToSave.keys()) });

195
src/UntrustedContent.ts Normal file
View File

@ -0,0 +1,195 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed 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.
*/
/**
* Utilities to deal with untrusted values coming from Matrix events.
*
* e.g. to confirm that a value `foo` has type `{ bar: string[]}`,
* run
* ```ts
* new SubTypeObjectContent({
* bar: STRING_CONTENT.array()
* }).check_type(value)
* ```
*/
/**
* The abstract root class for all content we wish to validate against.
*/
abstract class AbstractContent {
/**
* Validate the type of a value against `this`.
* @param value
*/
abstract checkType(value: any): boolean;
/**
* If `value` has `this` type, return `value`, otherwise
* return `defaults()`.
*/
fallback(value: any, defaults: () => any): any {
if (this.checkType(value)) {
return value;
}
return defaults();
}
/**
* Return an `AbstractContent` for values of type `this | null | undefined`.
*/
optional(): AbstractContent {
return new OptionalContent(this);
}
/**
* Return a `AbstractContent` for values of type `this[]`.
*
* This is a shortcut for `new OptionalContent(this)`
*/
array(): AbstractContent {
return new ArrayContent(this);
}
};
/**
* A content validator for numbers.
*/
class StringContent extends AbstractContent {
/**
* Check that `value` is a string.
*/
checkType(value: any): boolean {
return typeof value === "string";
}
};
/**
* A content validator for strings (singleton).
*/
export const STRING_CONTENT = new StringContent();
/**
* A content validator for numbers.
*/
class NumberContent extends AbstractContent {
checkType(value: any): boolean {
return typeof value === "number";
}
};
/**
* A content validator for numbers (singleton).
*/
export const NUMBER_CONTENT = new NumberContent();
/**
* A content validator for arrays.
*/
class ArrayContent extends AbstractContent {
constructor(public readonly content: AbstractContent) {
super()
}
/**
* Check that `value` is an array and that each value it contains
* has type `type.content`.
*/
checkType(value: any): boolean {
if (!Array.isArray(value)) {
return false;
}
for (let item of value) {
if (!this.content.checkType(item)) {
return false;
}
}
return true;
}
}
class OptionalContent extends AbstractContent {
constructor(public readonly content: AbstractContent) {
super()
}
optional(): AbstractContent {
return this;
}
/**
* Check that value either has type `this.content` or is `null` or `undefined`.
*/
checkType(value: any): boolean {
if (typeof value === "undefined") {
return true;
}
if (value === null) {
return true;
}
if (this.content.checkType(value)) {
return true;
}
return false;
}
}
export class SubTypeObjectContent extends AbstractContent {
constructor(public readonly fields: Record<string, AbstractContent>) {
super()
}
/**
* Check that `value` contains **at least** the fields of `this.fields`
* and that each field specified in `this.fields` holds a value that
* matches the type specified in `this.fields`.
*/
checkType(value: any): boolean {
if (typeof value !== "object") {
return false;
}
if (value === null) {
// Let's not forget that `typeof null === "object"`
return false;
}
if (Array.isArray(value)) {
// Let's not forget that `typeof [...] === "object"`
return false;
}
for (let [k, expected] of Object.entries(this.fields)) {
if (!expected.checkType(value[k])) {
return false;
}
}
return true;
}
}
export class ExactTypeObjectContent extends SubTypeObjectContent {
constructor(public readonly fields: Record<string, AbstractContent>) {
super(fields)
}
/**
* Check that `value` contains **exactly** the fields of `this.fields`
* and that each field specified in `this.fields` holds a value that
* matches the type specified in `this.fields`.
*/
checkType(value: any): boolean {
if (!super.checkType(value)) {
return false;
}
// Check that we don't have any field we're not expecting.
for (let k of Object.keys(value)) {
if (!(k in this.fields)) {
return false;
}
}
return true;
}
}

View File

@ -19,6 +19,10 @@ import PolicyList from "../models/PolicyList";
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk"; import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk";
import { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule"; import { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule";
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand"; import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";
import * as UntrustedContent from "../UntrustedContent";
const DEFAULT_LIST_EXPECTED_CONTENT = new UntrustedContent.SubTypeObjectContent({
shortcode: UntrustedContent.STRING_CONTENT
});
interface Arguments { interface Arguments {
list: PolicyList | null; list: PolicyList | null;
@ -31,7 +35,8 @@ interface Arguments {
export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise<Arguments | null> { export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise<Arguments | null> {
let defaultShortcode: string | null = null; let defaultShortcode: string | null = null;
try { try {
const data: { shortcode: string } = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE); const untrustedData = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE);
const data: { shortcode: string | null } = DEFAULT_LIST_EXPECTED_CONTENT.fallback(untrustedData, () => ({ shortcode: null }));
defaultShortcode = data['shortcode']; defaultShortcode = data['shortcode'];
} catch (e) { } catch (e) {
LogService.warn("UnbanBanCommand", "Non-fatal error getting default ban list"); LogService.warn("UnbanBanCommand", "Non-fatal error getting default ban list");

View File

@ -21,6 +21,7 @@ import { MatrixSendClient } from "../MatrixEmitter";
import AwaitLock from "await-lock"; import AwaitLock from "await-lock";
import { monotonicFactory } from "ulidx"; import { monotonicFactory } from "ulidx";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import * as UntrustedContent from "../UntrustedContent";
/** /**
* Account data event type used to store the permalinks to each of the policylists. * Account data event type used to store the permalinks to each of the policylists.
@ -33,6 +34,9 @@ import { Mjolnir } from "../Mjolnir";
* ``` * ```
*/ */
export const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists"; export const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
const WATCHED_LISTS_EXPECTED_CONTENT = new UntrustedContent.SubTypeObjectContent({
references: UntrustedContent.STRING_CONTENT.array()
});
/** /**
* A prefix used to record that we have already warned at least once that a PolicyList room is unprotected. * A prefix used to record that we have already warned at least once that a PolicyList room is unprotected.
@ -707,7 +711,13 @@ export class PolicyListManager {
let watchedListsEvent: { references?: string[] } | null = null; let watchedListsEvent: { references?: string[] } | null = null;
try { try {
watchedListsEvent = await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE); watchedListsEvent = WATCHED_LISTS_EXPECTED_CONTENT.fallback(
await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE),
() => {
LogService.warn('Mjolnir', "Invalid account data for Mjolnir's watched lists, assuming first start.");
return null;
}
);
} catch (e) { } catch (e) {
if (e.statusCode === 404) { if (e.statusCode === 404) {
LogService.warn('Mjolnir', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", extractRequestError(e)); LogService.warn('Mjolnir', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", extractRequestError(e));

View File

@ -31,6 +31,7 @@ import { htmlEscape } from "../utils";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
import { RoomUpdateError } from "../models/RoomUpdateError"; import { RoomUpdateError } from "../models/RoomUpdateError";
import { LocalAbuseReports } from "./LocalAbuseReports"; import { LocalAbuseReports } from "./LocalAbuseReports";
import * as UntrustedContent from "../UntrustedContent";
const PROTECTIONS: Protection[] = [ const PROTECTIONS: Protection[] = [
new FirstMessageIsImage(), new FirstMessageIsImage(),
@ -45,6 +46,10 @@ const PROTECTIONS: Protection[] = [
]; ];
const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections"; const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections";
const ENABLED_PROTECTIONS_EXPECTED_CONTENT = new UntrustedContent.SubTypeObjectContent({
enabled: UntrustedContent.STRING_CONTENT.array(),
}).optional();
const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence"; const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence";
/** /**
@ -87,7 +92,10 @@ export class ProtectionManager {
let enabledProtections: { enabled: string[] } | null = null; let enabledProtections: { enabled: string[] } | null = null;
try { try {
enabledProtections = await this.mjolnir.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE); enabledProtections = ENABLED_PROTECTIONS_EXPECTED_CONTENT.fallback(
await this.mjolnir.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE),
() => null
);
} catch { } catch {
// this setting either doesn't exist, or we failed to read it (bad network?) // this setting either doesn't exist, or we failed to read it (bad network?)
// TODO: retry on certain failures? // TODO: retry on certain failures?

View File

@ -1,6 +1,7 @@
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import { LogLevel } from "matrix-bot-sdk"; import { LogLevel } from "matrix-bot-sdk";
import ManagementRoomOutput from "../../src/ManagementRoomOutput"; import ManagementRoomOutput from "../../src/ManagementRoomOutput";
import * as UntrustedContent from "../../src/UntrustedContent";
describe("Test: utils", function() { describe("Test: utils", function() {
it("replaceRoomIdsWithPills correctly turns a room ID in to a pill", async function() { it("replaceRoomIdsWithPills correctly turns a room ID in to a pill", async function() {
@ -32,3 +33,173 @@ describe("Test: utils", function() {
}); });
}); });
describe("Test: UntrustedContent", function() {
it("accepts valid content and rejects invalid content", async function() {
/**
* IMPORTANT NOTE
*
* For some reason, `assert()` gets its source tracking wrong. If you need to check an error in this file,
* look at the line number in the stack trace, not at what `assert()` prints out!
*/
// Numbers
assert(UntrustedContent.NUMBER_CONTENT.checkType(100));
assert(UntrustedContent.NUMBER_CONTENT.checkType(-100));
assert(UntrustedContent.NUMBER_CONTENT.checkType(NaN));
assert(UntrustedContent.NUMBER_CONTENT.checkType(Number.NEGATIVE_INFINITY));
assert(UntrustedContent.NUMBER_CONTENT.checkType(Number.POSITIVE_INFINITY));
assert(! UntrustedContent.NUMBER_CONTENT.checkType(null));
assert(! UntrustedContent.NUMBER_CONTENT.checkType(undefined));
assert(! UntrustedContent.NUMBER_CONTENT.checkType(""));
assert(! UntrustedContent.NUMBER_CONTENT.checkType("foobar"));
assert(! UntrustedContent.NUMBER_CONTENT.checkType(true));
assert(! UntrustedContent.NUMBER_CONTENT.checkType(false));
assert(! UntrustedContent.NUMBER_CONTENT.checkType({}));
assert(! UntrustedContent.NUMBER_CONTENT.checkType([]));
// Strings
assert(UntrustedContent.STRING_CONTENT.checkType(""));
assert(UntrustedContent.STRING_CONTENT.checkType("<>"));
assert(UntrustedContent.STRING_CONTENT.checkType(`${"template"}`));
assert(! UntrustedContent.STRING_CONTENT.checkType(null));
assert(! UntrustedContent.STRING_CONTENT.checkType(undefined));
assert(! UntrustedContent.STRING_CONTENT.checkType(0));
assert(! UntrustedContent.STRING_CONTENT.checkType(true));
assert(! UntrustedContent.STRING_CONTENT.checkType(false));
assert(! UntrustedContent.STRING_CONTENT.checkType({}));
assert(! UntrustedContent.STRING_CONTENT.checkType([]));
// Number Arrays
assert(UntrustedContent.NUMBER_CONTENT.array().checkType([]));
assert(UntrustedContent.NUMBER_CONTENT.array().checkType([1, 2, 3, 4]));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType(null));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType(undefined));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType(""));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType(0));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType("foobar"));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType(true));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType(false));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType({}));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType([null]));
assert(! UntrustedContent.NUMBER_CONTENT.array().checkType([undefined]));
// String Arrays
assert(UntrustedContent.STRING_CONTENT.array().checkType([]));
assert(UntrustedContent.STRING_CONTENT.array().checkType(["1", "2", "3", "4"]));
assert(! UntrustedContent.STRING_CONTENT.array().checkType(null));
assert(! UntrustedContent.STRING_CONTENT.array().checkType(undefined));
assert(! UntrustedContent.STRING_CONTENT.array().checkType(""));
assert(! UntrustedContent.STRING_CONTENT.array().checkType(0));
assert(! UntrustedContent.STRING_CONTENT.array().checkType("foobar"));
assert(! UntrustedContent.STRING_CONTENT.array().checkType(true));
assert(! UntrustedContent.STRING_CONTENT.array().checkType(false));
assert(! UntrustedContent.STRING_CONTENT.array().checkType({}));
assert(! UntrustedContent.STRING_CONTENT.array().checkType([null]));
assert(! UntrustedContent.STRING_CONTENT.array().checkType([undefined]));
// Optional numbers
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(null));
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(undefined));
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(100));
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(-100));
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(NaN));
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(Number.NEGATIVE_INFINITY));
assert(UntrustedContent.NUMBER_CONTENT.optional().checkType(Number.POSITIVE_INFINITY));
assert(! UntrustedContent.NUMBER_CONTENT.optional().checkType(""));
assert(! UntrustedContent.NUMBER_CONTENT.optional().checkType("foobar"));
assert(! UntrustedContent.NUMBER_CONTENT.optional().checkType(true));
assert(! UntrustedContent.NUMBER_CONTENT.optional().checkType(false));
assert(! UntrustedContent.NUMBER_CONTENT.optional().checkType({}));
assert(! UntrustedContent.NUMBER_CONTENT.optional().checkType([]));
// Optional strings
assert(UntrustedContent.STRING_CONTENT.optional().checkType(null));
assert(UntrustedContent.STRING_CONTENT.optional().checkType(undefined));
assert(UntrustedContent.STRING_CONTENT.optional().checkType(""));
assert(UntrustedContent.STRING_CONTENT.optional().checkType("<>"));
assert(UntrustedContent.STRING_CONTENT.optional().checkType(`${"template"}`));
assert(! UntrustedContent.STRING_CONTENT.optional().checkType(0));
assert(! UntrustedContent.STRING_CONTENT.optional().checkType(true));
assert(! UntrustedContent.STRING_CONTENT.optional().checkType(false));
assert(! UntrustedContent.STRING_CONTENT.optional().checkType({}));
assert(! UntrustedContent.STRING_CONTENT.optional().checkType([]));
// Optional arrays
assert(UntrustedContent.NUMBER_CONTENT.array().optional().checkType(null));
assert(UntrustedContent.NUMBER_CONTENT.array().optional().checkType(undefined));
assert(UntrustedContent.NUMBER_CONTENT.array().optional().checkType([]));
assert(UntrustedContent.NUMBER_CONTENT.array().optional().checkType([1, 2, 3, 4]));
assert(UntrustedContent.STRING_CONTENT.array().optional().checkType(null));
assert(UntrustedContent.STRING_CONTENT.array().optional().checkType(undefined));
assert(UntrustedContent.STRING_CONTENT.array().optional().checkType([]));
assert(UntrustedContent.STRING_CONTENT.array().optional().checkType(["1", "2", "3", "4"]));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType(""));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType(0));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType("foobar"));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType(true));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType(false));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType({}));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType([null]));
assert(! UntrustedContent.NUMBER_CONTENT.array().optional().checkType([undefined]));
// Arrays of optionals
assert(UntrustedContent.NUMBER_CONTENT.optional().array().checkType([]));
assert(UntrustedContent.NUMBER_CONTENT.optional().array().checkType([1, 2, 3, 4]));
assert(UntrustedContent.NUMBER_CONTENT.optional().array().checkType([1, 2, 3, 4, null]));
assert(UntrustedContent.NUMBER_CONTENT.optional().array().checkType([1, 2, 3, 4, undefined]));
assert(! UntrustedContent.NUMBER_CONTENT.optional().array().checkType([1, 2, 3, 4, undefined, "foobar"]));
assert(UntrustedContent.STRING_CONTENT.optional().array().checkType([]));
assert(UntrustedContent.STRING_CONTENT.optional().array().checkType(["1", "2", "3", "4"]));
assert(UntrustedContent.STRING_CONTENT.optional().array().checkType(["1", "2", "3", "4", null]));
assert(UntrustedContent.STRING_CONTENT.optional().array().checkType(["1", "2", "3", "4", undefined]));
assert(! UntrustedContent.STRING_CONTENT.optional().array().checkType(["1", "2", "3", "4", undefined, 5]));
// Subtype objects
assert(new UntrustedContent.SubTypeObjectContent({}).checkType({}));
assert(new UntrustedContent.SubTypeObjectContent({}).checkType({"foo": 1}));
assert(! new UntrustedContent.SubTypeObjectContent({}).checkType(null));
assert(! new UntrustedContent.SubTypeObjectContent({}).checkType(undefined));
assert(! new UntrustedContent.SubTypeObjectContent({}).checkType(0));
assert(! new UntrustedContent.SubTypeObjectContent({}).checkType(true));
assert(! new UntrustedContent.SubTypeObjectContent({}).checkType(false));
assert(! new UntrustedContent.SubTypeObjectContent({}).checkType([]));
assert(new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": 1}));
assert(new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": 1, "bar": "sna"}));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(null));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(undefined));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(0));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(true));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(false));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType([]));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({}));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": null}));
assert(! new UntrustedContent.SubTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": "string"}));
// Exact objects
assert(new UntrustedContent.ExactTypeObjectContent({}).checkType({}));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType({"foo": 1}));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType(null));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType(undefined));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType(0));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType(true));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType(false));
assert(! new UntrustedContent.ExactTypeObjectContent({}).checkType([]));
assert(new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": 1}));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": 1, "bar": "sna"}));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(null));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(undefined));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(0));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(true));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType(false));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType([]));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({}));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": null}));
assert(! new UntrustedContent.ExactTypeObjectContent({"foo": UntrustedContent.NUMBER_CONTENT}).checkType({"foo": "string"}));
});
});