preactjs / preactjs/preact-render-to-string

HTML Entity encoding in Script Tags

Open
#332 5 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
727
Forks
101
Avg merge
3d 6h
Merged PRs (30d)
6

Description

Background

I'm new to both 'preact' and 'preact-render-to-string' (and htm) and was surprised to find the contents of my script tag were automatically being entity encoded, which unfortunately breaks it.

Expected Output (Non-Encoded):
<script type="importmap">
  { "imports": {
    "lazyload": "./lazyload.js" }
  }
</script>

<script type="module">
  const iMap = { "imports": {
    "lazyload": "./lazyload.js" }
  };
</script>
Actual Output (Encoded):
<!-- Browser Error: "Could not parse Json" -->
<script type="importmap">
  { &quot;imports&quot;: {
    &quot;lazyload&quot;: &quot;./lazyload.js&quot; }
  }
</script>

<!-- Browser Error: "Unexpected token '&'" at '&'quot; -->
<script type="module">
  const iMap = { &quot;imports&quot;: {
    &quot;lazyload&quot;: &quot;./lazyload.js&quot; }
  };
</script>

Since any content within a script tag is parsed by the appropriate scripting-engine rather than the html-parser and thus doesn't need to be entity escaped, it might be convenient to exclude the 'script' tag from automagical encoding.

NOTE: I'm not sure if this affects non-SSR preact. I haven't gotten far enough to try client-side rendering yet.

Research

It looks like it would be trivial to implement in src/index.js:
Around Line 470 add something like...

else if (typeof children === 'string') {
  if (type === 'script') { html = children; }
  else { html = encodeEntities(children); }
}

However, I also see around line 148 'Text VNodes: escape as HTML' and I don't know enough about preact to know how that would be triggered.

Fortunately, by looking at the source code I did find dangerouslySetInnerHTML which solves my immediate problem.

Discussion

I'm not sure what the best solution here would be.

In-my-own-opinion, modern web-apps have grown so massive that the use of inline-scripting (rather than loading an external script file) is a pretty fringe-case. I'm only using it myself because I'm still in the early-hacky-days of my current project.

Further, enforcing the use of dangerouslySetInnerHTML does provide a certain additional level of security against accidental... stuff. Though it could be better documented, I had to dig through the source-code to find it.

On the other hand, web technologies are rapidly expanding with new use-cases; 'import maps' being an easy example. And using dangerouslySetInnerHTML on an element that doesn't technically need to be entity escaped to begin with... well, it just isn't 'pretty'.

I think that's actually what bothers me the most, my code isn't as pretty anymore.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the script-tag handling around line 470 in src/index.js, then compare it with the text VNode escaping logic around line 148. Check how dangerouslySetInnerHTML currently avoids the issue and determine whether script contents should be rendered unencoded without weakening normal HTML escaping. Done means script and import-map output remains valid while other text is still safely encoded.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.