Skip to content

Commit

Permalink
Sanitize JSON Objects. Return santized values in validate methods. (#7)
Browse files Browse the repository at this point in the history
BREAKING: Change `isValidHtml` method to `validate`.
Return object instead of boolean.
  • Loading branch information
ssylvia authored May 14, 2018
1 parent 5d8baeb commit e3a4cf9
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 29 deletions.
56 changes: 48 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @esri/arcgis-html-sanitizer

This utility is a simple wrapper around the [js-xss](https://github.com/leizongmin/js-xss) library that will configure `js-xss` to sanitize strings according to the [ArcGIS Supported HTML spec](https://doc.arcgis.com/en/arcgis-online/reference/supported-html.htm). It also
This utility is a simple wrapper around the [js-xss](https://github.com/leizongmin/js-xss) library that will configure `js-xss` to sanitize a value according to the [ArcGIS Supported HTML spec](https://doc.arcgis.com/en/arcgis-online/reference/supported-html.htm). It also
includes a few additional helper methods to validate strings and
prevent XSS attacks.

Expand Down Expand Up @@ -36,10 +36,11 @@ article: https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_C
* [Versioning](#versioning)
* [Contributing](#contributing)
* [License](#license)
* [Dependencies](#dependencies)

### Why [`js-xss`](https://github.com/leizongmin/js-xss)?

[`js-xss`](https://github.com/leizongmin/js-xss) is lightweight (5.5k gzipped)
[js-xss](https://github.com/leizongmin/js-xss) is lightweight (5.5k gzipped)
library with an [MIT](https://github.com/leizongmin/js-xss#license) license. It is also highly customizable
and works well in both Node.js applications and in the browser.

Expand Down Expand Up @@ -88,7 +89,7 @@ Load as script tag
<script src="path/to/arcgis-html-sanitizer.min.js"></script>

<!-- CDN (Adjust the version as needed) -->
<script src="https://cdn.jsdelivr.net/npm/@esri/arcgis-html-sanitizer@0.2.0/dist/umd/arcgis-html-sanitizer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@esri/arcgis-html-sanitizer@0.3.0/dist/umd/arcgis-html-sanitizer.min.js"></script>
```

#### Basic Usage
Expand All @@ -97,16 +98,50 @@ Load as script tag
// Instantiate a new Sanitizer object
const sanitizer = new Sanitizer();

// Check if a string contains invalid HTML
const isValid = sanitizer.isValidHtml(
// Sanitize a string
const sanitizedHtml = sanitizer.sanitize(
'<img src="https://example.com/fake-image.jpg" onerror="alert(1);" />'
);
// isValid => false
// sanitizedHtml => <img src="https://example.com/fake-image.jpg" />

const sanitizedHtml = sanitizer.sanitize(
// Check if a string contains invalid HTML
const validation = sanitizer.validate(
'<img src="https://example.com/fake-image.jpg" onerror="alert(1);" />'
);
// sanitizedHtml => <img src="https://example.com/fake-image.jpg" />
// validation => {
// isValid: false
// sanitized: '<img src="https://example.com/fake-image.jpg" />'
// }
```

#### Sanitize JSON

In addition to sanitizing strings, this utility also allows you to sanitize full
JSON objects. This can be useful if you want to sanitize all the app data
returned from the server before using it in your application.

If the value passed does not contain a valid JSON data type (String,
Number, JSON Object, Array, Boolean, or null), the value will be nullified.

**WARNING**: You should never concatenate strings from multiple values in the
JSON. Strings values may be safe and pass the sanitizer when separated but
become dangerous after they are concatenated. This is intended as a convenience
method only. You should run the strings through the sanitizer again after they
have been concatenated.

```js
// Instantiate a new Sanitizer object
const sanitizer = new Sanitizer();

// Deeply sanitize a JSON object
const sanitizedJSON = sanitizer.sanitize({
sample: ['<img src="https://example.com/fake-image.jpg" onerror="alert(1);\
" />']
});

// sanitizedJSON => {
// "sample": ["<img src=\"https://example.com/fake-image.jpg\" />"]
// }
```

#### Customizing Filter Options
Expand Down Expand Up @@ -195,3 +230,8 @@ See the License for the specific language governing permissions and
limitations under the License.

A copy of the license is available in the repository's [LICENSE](./LICENSE) file.

### Dependencies

* [js-xss](https://github.com/leizongmin/js-xss) ([MIT](https://github.com/leizongmin/js-xss#license))
* [Lodash isPlainObject](https://www.npmjs.com/package/lodash.isplainobject) ([MIT](https://raw.githubusercontent.com/lodash/lodash/4.17.10-npm/LICENSE))
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@esri/arcgis-html-sanitizer",
"version": "0.2.0",
"version": "0.3.0",
"description":
"A simple utility to sanitize a string according to ArcGIS supported HTML specification.",
"main": "dist/node/index.js",
Expand Down Expand Up @@ -42,11 +42,12 @@
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
},
"dependencies": {
"lodash.isplainobject": "^4.0.6",
"xss": "^0.3.8"
},
"devDependencies": {
"@types/jest": "^22.2.0",
"@types/lodash.merge": "^4.6.3",
"@types/lodash.isplainobject": "^4.0.3",
"jest": "^22.4.2",
"ts-jest": "^22.4.1",
"ts-loader": "^4.0.1",
Expand Down
147 changes: 139 additions & 8 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,142 @@ describe('Sanitizer', () => {
expect(sanitizer4.xssFilterOptions).toEqual({ whiteList: { a: [] } });
});

test('sanitizes invalid html', () => {
test('sanitizes a value', () => {
const sanitizer = new Sanitizer();

// Numbers
expect(sanitizer.sanitize(NaN)).toBe(null);
expect(sanitizer.sanitize(Infinity)).toBe(null);
expect(sanitizer.sanitize(123)).toBe(123);
expect(sanitizer.sanitize(123)).toBe(123);

// Boolean
expect(sanitizer.sanitize(true)).toBe(true);
expect(sanitizer.sanitize(false)).toBe(false);

// Strings
const basicString = 'Hello World';
const validHtml = 'Hello <a href="https://example.org">Link</a>';
const invalidHtml =
'Evil <img src="https://exmaple.org/myImg.jpg" onerror="alert(1)" />';
const sanitizedInvalidHtml =
'Evil <img src="https://exmaple.org/myImg.jpg" />';

const sanitizer = new Sanitizer();

expect(sanitizer.sanitize(basicString)).toBe(basicString);
expect(sanitizer.sanitize(validHtml)).toBe(validHtml);
expect(sanitizer.sanitize(invalidHtml)).toBe(sanitizedInvalidHtml);

// Built in Objects:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
// Value Properties (Infinity and NanN defined in Numbers above)
expect(sanitizer.sanitize(undefined)).toBe(null);
expect(sanitizer.sanitize(null)).toBe(null);

// Fundamental objects
expect(sanitizer.sanitize(Object)).toBe(null);
expect(sanitizer.sanitize(Function)).toBe(null);
expect(sanitizer.sanitize(Boolean)).toBe(null);
expect(sanitizer.sanitize(Symbol)).toBe(null);
expect(sanitizer.sanitize(Error)).toBe(null);
expect(sanitizer.sanitize(EvalError)).toBe(null);
expect(sanitizer.sanitize(RangeError)).toBe(null);
expect(sanitizer.sanitize(ReferenceError)).toBe(null);
expect(sanitizer.sanitize(SyntaxError)).toBe(null);
expect(sanitizer.sanitize(TypeError)).toBe(null);
expect(sanitizer.sanitize(URIError)).toBe(null);

// Number and dates
expect(sanitizer.sanitize(Number)).toBe(null);
expect(sanitizer.sanitize(Math)).toBe(null);
expect(sanitizer.sanitize(Date)).toBe(null);
expect(sanitizer.sanitize(new Date())).toBe(null);

// Text processing
expect(sanitizer.sanitize(String)).toBe(null);
expect(sanitizer.sanitize(RegExp)).toBe(null);
expect(sanitizer.sanitize(/\w+/)).toBe(null);

// Indexed collections
expect(sanitizer.sanitize(Array)).toBe(null);
expect(sanitizer.sanitize(Int8Array)).toBe(null);
expect(sanitizer.sanitize(Uint8Array)).toBe(null);
expect(sanitizer.sanitize(Uint8ClampedArray)).toBe(null);
expect(sanitizer.sanitize(Int16Array)).toBe(null);
expect(sanitizer.sanitize(Uint16Array)).toBe(null);
expect(sanitizer.sanitize(Int32Array)).toBe(null);
expect(sanitizer.sanitize(Uint32Array)).toBe(null);
expect(sanitizer.sanitize(Float32Array)).toBe(null);
expect(sanitizer.sanitize(Float64Array)).toBe(null);

// Keyed collections
expect(sanitizer.sanitize(Map)).toBe(null);
expect(sanitizer.sanitize(Set)).toBe(null);
expect(sanitizer.sanitize(WeakMap)).toBe(null);
expect(sanitizer.sanitize(WeakSet)).toBe(null);

// Structured Data
expect(sanitizer.sanitize(ArrayBuffer)).toBe(null);
expect(sanitizer.sanitize(DataView)).toBe(null);
expect(sanitizer.sanitize(JSON)).toBe(null);

// Control abstraction objects
expect(sanitizer.sanitize(Promise)).toBe(null);

// Reflection
expect(sanitizer.sanitize(Reflect)).toEqual({});
expect(sanitizer.sanitize(Proxy)).toBe(null);

// Internationalization
expect(sanitizer.sanitize(Intl)).toEqual({});
expect(sanitizer.sanitize(Intl.Collator)).toBe(null);
expect(sanitizer.sanitize(Intl.DateTimeFormat)).toBe(null);
expect(sanitizer.sanitize(Intl.NumberFormat)).toBe(null);

// Others
expect(sanitizer.sanitize(arguments)).toBe(null);
expect(sanitizer.sanitize(() => 'test')).toBe(null);
expect(sanitizer.sanitize(new Error('test'))).toBe(null);
});

test('deeply sanitizes an object', () => {
const sanitizer = new Sanitizer();

// If object is clean, it return the exact same object;
const cleanObj1 = {
a: null,
b: true,
c: 'clean string'
};
const result1 = sanitizer.sanitize(cleanObj1);
expect(result1).toBe(cleanObj1);

// Sanitizes dirty object
const result2 = sanitizer.sanitize({
a: 1,
b: true,
c: 'clean string',
d: 'Evil <img src="https://exmaple.org/myImg.jpg" onerror="alert(1)" />',
e: [
1,
true,
'Evil <img src="https://exmaple.org/myImg.jpg" onerror="alert(1)" />',
['inner', 'array']
],
f: new Date()
});
const expected2 = {
a: 1,
b: true,
c: 'clean string',
d: 'Evil <img src="https://exmaple.org/myImg.jpg" />',
e: [
1,
true,
'Evil <img src="https://exmaple.org/myImg.jpg" />',
['inner', 'array']
],
f: null
};
expect(result2).toEqual(expected2);
});

test('checks if string is valid html', () => {
Expand All @@ -63,21 +186,29 @@ describe('Sanitizer', () => {

const sanitizer = new Sanitizer();

expect(sanitizer.isValidHtml(basicString)).toBe(true);
expect(sanitizer.isValidHtml(validHtml)).toBe(true);
expect(sanitizer.isValidHtml(invalidHtml)).toBe(false);
expect(sanitizer.validate(basicString).isValid).toBe(true);
expect(sanitizer.validate(validHtml).isValid).toBe(true);
expect(sanitizer.validate(invalidHtml).isValid).toBe(false);
});

test('extends an object of array by concatenating arrays', () => {
// tslint:disable-next-line:no-string-literal
const privateExtend = new Sanitizer()['_extendObjectOfArrays'];
const _extendObjectOfArrays = new Sanitizer()['_extendObjectOfArrays'];

const result = privateExtend([
const result = _extendObjectOfArrays([
{ a: [1, 2] },
{ a: [3, 4], b: [1, 2] },
{ b: [3, 4] }
]);

expect(result).toEqual({ a: [1, 2, 3, 4], b: [1, 2, 3, 4] });
});

test('returns null of iteration fails', () => {
// tslint:disable-next-line:no-string-literal
const _iterateOverObject = new Sanitizer()['_iterateOverObject'];

// Will fail because "this" is not defined
expect(_iterateOverObject({ a: 1 })).toBe(null);
});
});
Loading

0 comments on commit e3a4cf9

Please sign in to comment.