Skip to content

vite 6 breaks ability to using special chars in html tags attrs #18811

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
7 tasks done
brokenmass opened this issue Nov 27, 2024 · 15 comments · May be fixed by #19396
Open
7 tasks done

vite 6 breaks ability to using special chars in html tags attrs #18811

brokenmass opened this issue Nov 27, 2024 · 15 comments · May be fixed by #19396
Labels
p2-to-be-discussed Enhancement under consideration (priority)

Comments

@brokenmass
Copy link

brokenmass commented Nov 27, 2024

Describe the bug

when using ejs tags (like html: {cspNonce: '<%= nonce _%>'}, or experimental: { renderBuiltUrl(filename) {return <%= somePath %>/${filename};}})) , or URLS that contains multiple query strings parameters http:host.com/url?query=123&limit=100, in any html tag attribute, the ", ', &, <, and >. are escaped and so the output html is no longer correct.

(the typical use case for the ejs approach is to have express render the built html page and populate those template variables at runtime).

The bug is not present in vite 5 and was introduced by #18067 , to fix issue #18040.
The issue seems to me to have no actual foundation (cause if some malicious function is able to inject a tag, escaping attrs is pretty much useless as code can be injected in much easier ways anyway) and the fix mangle every attrs that use the above special character.

Reverting the PR resolves the problem.

Reproduction

n/a

Steps to reproduce

No response

System Info

n/a

Used Package Manager

npm

Logs

No response

Validations

@brokenmass brokenmass changed the title vite 6 breaks when using ejs style tags in attrs vite 6 breaks ability to using special chars in html tags attrs Nov 27, 2024
Copy link

Hello @brokenmass. Please provide a minimal reproduction using a GitHub repository or StackBlitz. Issues marked with needs reproduction will be closed if they have no activity within 3 days.

@hi-ogawa
Copy link
Collaborator

The use case seems legitimate, but it would be nice to have a reproduction. I'm not sure what you mean by the last one about "URLS that contains multiple query strings".

@terry-au
Copy link

terry-au commented Nov 28, 2024

@hi-ogawa I'm also encountering this problem. Here's an excerpt of the plugin we're using:

const entrypointPlugin: Plugin = {
  name: 'entrypoint-plugin',
  apply: 'build',
  enforce: 'pre',
  config(config): UserConfig {
    return {
      experimental: {
        renderBuiltUrl: (filename, { hostType }) => {
          if (hostType === 'html') {
            return `<%= cdn.baseUrl %>/${filename}`;
          }
          return undefined;
        },
      },
    };
  },
};

Vite 5:

<script type="module" crossorigin src="<%= cdn.baseUrl %>/assets/index-yAKNFxux.js"></script>

Vite 6:

<script type="module" crossorigin src="&lt;%= cdn.baseUrl %&gt;/assets/index-yAKNFxux.js"></script>

@brokenmass
Copy link
Author

brokenmass commented Nov 28, 2024

The use case seems legitimate, but it would be nice to have a reproduction. I'm not sure what you mean by the last one about "URLS that contains multiple query strings".

Link : https://stackblitz.com/edit/vitejs-vite-taxulw

vite.config.ts

import { defineConfig } from 'vite';

export default defineConfig({
  html: {
    cspNonce: '<%= cspNonce %>',
  },
  experimental: {
    renderBuiltUrl: (filename, { hostType }) => {
      if (hostType === 'html') {
        return `<%= cdn.baseUrl %>/${filename}`;
      }
      return undefined;
    },
  },
  plugins: [
    {
      name: 'inject-tag',
      enforce: 'post',
      transformIndexHtml: () => {
        return [
          {
            tag: 'image',
            injectTo: 'body',
            attrs: {
              src: 'https://placehold.co/600x400?text=Hello+World&font=Oswald',
            },
          },
        ];
      },
    },
  ],
});

run the command npm install && npm run preview

with vite 5

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <meta property="csp-nonce" nonce="<%= cspNonce %>">
    <script type="module" crossorigin src="<%= cdn.baseUrl %>/assets/index-MQ_MSdzi.js" nonce="<%= cspNonce %>"></script>
    <link rel="stylesheet" crossorigin href="<%= cdn.baseUrl %>/assets/index-DRNMkqs8.css" nonce="<%= cspNonce %>">
  </head>
  <body>
    <div id="app"></div>
    <image src="https://placehold.co/600x400?text=Hello+World&font=Oswald"></image>
  </body>
</html>

with vite 6

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <meta property="csp-nonce" nonce="&lt;%= cspNonce %&gt;">
    <script type="module" crossorigin src="&lt;%= cdn.baseUrl %&gt;/assets/index-MQ_MSdzi.js" nonce="<%= cspNonce %>"></script>
    <link rel="stylesheet" crossorigin href="&lt;%= cdn.baseUrl %&gt;/assets/index-DRNMkqs8.css" nonce="<%= cspNonce %>">
  </head>
  <body>
    <div id="app"></div>
    <image src="https://placehold.co/600x400?text=Hello+World&amp;font=Oswald"></image>
  </body>
</html>

notice how in the second output the special characters <, > and & have been converted to their html escaped versions (&lt;, &gt and &amp; respectively).

the URLS that contains multiple query strings parameters issue is highlighted bythe injected placeholder image. multiple query string parameters are concatenated with & that in vite 6 gets escaped. Buy that's actually not a real issue as the browser seems to interpret that correctly.

Seems like this should be quite a relevant issue, no ?

@bluwy
Copy link
Member

bluwy commented Nov 28, 2024

For me, this feels more of an intentional breaking change (that we didn't intend to be very breaking but should've mentioned in the migration guide anyways) than a bug. Vite couldn't tell that you're specifying a string as a placeholder that shouldn't be escaped. And escaping would've been the safer default.

I think it should be possible to migrate / adapt by using URL-safe characters instead? Or is using the specific <%= syntax required?

@brokenmass
Copy link
Author

For me, this feels more of an intentional breaking change (that we didn't intend to be very breaking but should've mentioned in the migration guide anyways) than a bug.
Vite couldn't tell that you're specifying a string as a placeholder that shouldn't be escaped. And escaping would've been the safer default.

Yes i get your point, but HTML-Escaping attributes in a build tools does not seem the standard behaviour, and is definitely an unexpected one.
The output of the compilation does not have to be fully valid html as there might be some other task or services that parse/transform the html before serving it.

I think it should be possible to migrate / adapt by using URL-safe characters instead? Or is using the specific <%= syntax required?

Sure i think it's possible to use custom delimiter or move to a different template engine, like pug. but would avoid it if possible.

As a solution, if you do not want to revert the pr, you could allow an option to opt out of this defensive behaviour

@sapphi-red
Copy link
Member

While we escape " / ' / & / < / > now, I guess we only need to escape & and " because we know that the value will be used as an attribute with double quotes.

@terry-au
Copy link

terry-au commented Nov 29, 2024

Maybe we should consider letting renderBuiltUrl return its string output unmodified, since its purpose is to provide precise control over asset paths. As an alternative, we could explore adding an option to skip string escaping by returning:

{
  rawUrl: `<%= cdn.baseUrl %>/${filename}`,
}

@brokenmass
Copy link
Author

brokenmass commented Nov 29, 2024

While we escape " / ' / & / < / > now, I guess we only need to escape & and " because we know that the value will be used as an attribute with double quotes.

Yes, just html-escaping the double quotes should be enough. Why also the & ?
But again what’s the requirement here ? Protect from self code injection ? Seems like a solution to a non-problem, and even partial one as then you should escape the tag name too, and probably many other locations.

@brokenmass
Copy link
Author

bump

@sapphi-red
Copy link
Member

Why also the & ?

Because otherwise the output will be partially escaped. For example, &quote;" will be &quote;&quote; and the consumer cannot know if &quote; should be read as " or &quote;. If & is escaped, it will be &amp;quote;&quote; and the consumer can know it is &quote;".

what’s the requirement here ?

To generate a valid HTML when a user returned an array from transformIndexHtml hook.
For example, when the following hook is used:

transformIndexHtml: () => {
  return [
    {
      tag: 'div',
      injectTo: 'body',
      attrs: {
        "data-foo": '"foo',
      },
    },
  ];
},

@brokenmass
Copy link
Author

brokenmass commented Dec 5, 2024

what’s the requirement here ?

To generate a valid HTML when a user returned an array from transformIndexHtml hook. For example, when the following hook is used:

sure but why should that be vite responsibility, why should it be vite requirement ?
There’s an option to inject tags, if the user want, then they can escape the attributes themselves; otherwise the build tool should just “trust” that the user knows what they are doing and leave it unmodified.

Actually this change break the build for everyone already escaping their injected attrs.

Remember this is not an externally defined input that can be hijacked, it’s a build instruction. And, I mean, it has been like this for years in vite and in every other tools before vite, like webpack.
Being defensive ONLY in the tag attributes makes very little sense cause if you want to protect the user from every invalid html then you should escape stuff everywhere (in your example you should escape the tag field too), and possibly even validate/rewrite the input index.html.

Escaping just the double quote to avoid escaping the attr by mistake seems the most reasonable approach to me.if the contents already contains & those should not be escaped (cause maybe the user already escaped the value) and leaving it unescaped does not break anything.

who’s going to take the final decision if this escaping is going to be reverted or not ? Any ETA ?

@brokenmass
Copy link
Author

@hi-ogawa would be nice to know what's the decision on this, as if the mentioned pr is not going to be reverted we need to find an alternative fix.

@tsotimus
Copy link

tsotimus commented Jan 4, 2025

Any movement on this? Just to add another use case which is mentioned above.

Inserting a Content Security Policy via transformIndexHtml is outputting some funky stuff - which I would argue is more important than the body examples given above.

To generate a valid HTML when a user returned an array from transformIndexHtml hook.

If this is the goal, the second example below is more valid than the first id argue

Why are we escaping these tags? I agree with @brokenmass here, I think this is Vite is overstepping here

// Inside dist/index.html with Vite 6
<meta http-equiv="Content-Security-Policy" content=" default-src &#39;self&#39;; img-src &#39;self&#39; data:; script-src-elem &#39;self&#39;; style-src-elem &#39;self&#39;; base-uri &#39;self&#39;; worker-src * blob:; font-src &#39;self&#39;;">

// What it should be and what the browser interprets the above as
<meta http-equiv="Content-Security-Policy" content=" default-src 'self'; img-src 'self' data:; script-src-elem 'self'; style-src-elem 'self'; base-uri 'self'; worker-src * blob:; font-src 'self';">

Run npm run build and npm run preview on this StackBlitz to see this for yourself.

@brokenmass
Copy link
Author

@hi-ogawa any update ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p2-to-be-discussed Enhancement under consideration (priority)
Projects
Status: Has plan
Development

Successfully merging a pull request may close this issue.

6 participants