wcoder / highlightjs-line-numbers.js

Line numbering plugin for Highlight.js
https://wcoder.github.io/highlightjs-line-numbers.js/
MIT License
551 stars 126 forks source link

Only the first line gets numbered when fetching data from an API and rendering it with highlightjs #86

Closed elaine-jackson closed 3 years ago

elaine-jackson commented 3 years ago

Describe the bug I have an application which uses the JavaScript fetch() API to fetch some data and render it on a page.

To Reproduce

<html>
<body>
<!--StartFragment-->

Line wrap
--
  | <!DOCTYPE html>
  |  
  | <html>
  |  
  | <head>
  | <title>Paste.is</title>
  | <meta name="viewport" content="width=device-width, initial-scale=1.0">
  |  
  | <link rel="stylesheet" href="/global.css" crossorigin="anonymous">
  | <link rel="stylesheet" href="/lib/bootstrap.min.css" crossorigin="anonymous">
  | <link rel="stylesheet" href="/lib/fontawesome-free-5.15.3-web/css/all.css" crossorigin="anonymous">
  |  
  | <script src="/lib/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
  | <script src="/lib/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
  | <script src="/lib/bootstrap.bundle.min.js" integrity="sha384-LtrjvnR4Twt/qOuYxE721u19sVFLVSA4hf/rRt6PrZTmiPltdZcI7q7PXQBYTKyf" crossorigin="anonymous"></script>
  | <script src="/lib/crypto-js.min.js" integrity="sha384-0DrKBsfUuJe/vqjia1HviapRn4mR1BYfCpQ9gT7qjSKu8TrzTe2tlbK3cI9i9EwV" crossorigin="anonymous"></script>
  | <script src="/lib/highlight.min.js"></script>
  | <script src="/lib/highlightjs-line-numbers.min.js"></script>
  |  
  | <script src="/paste.js?v=1620658068"></script>
  | </head>
  |  
  | <body>
  |  
  | <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  | <a class="navbar-brand" href="/">Paste.is</a>
  | <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="true" aria-label="Toggle navigation">
  | <span class="navbar-toggler-icon"></span>
  | </button>
  | <div class="navbar-collapse" id="navbarText">
  | <ul class="navbar-nav mr-auto">
  | <li class="nav-item active">
  | <a class="nav-link" href="/"><span class="fas fa-home"></span> Home <span class="sr-only">(current)</span></a>
  | </li>
  | <li class="nav-item active">
  | <a class="nav-link" href="mailto:abuse@paste.is"><span class="fas fa-exclamation-triangle"></span> Report Abuse <span class="sr-only">(current)</span></a>
  | </li>
  | </ul>
  | </div>
  | </nav>
  | <div class="alert alert-primary">
  | <strong>Introducing Encrypted Pastes:</strong> Encrypted Pastes are now in public beta there may still be bugs but we hope you enjoy the feature 😎.
  | </div>
  |  
  | <div class="card">
  | <div class="card-body">
  | <h5 class="card-title">View Paste</h5>
  | <pre><code id="pasteContent"></code></pre>
  | <hr />
  | <h5 class="card-title">Additional Information</h5>
  | <ul>
  | <li><strong>UUID:</strong> <span id="uuid"></span></li>
  | <li><strong>Published On:</strong> <span id="pub"></span></li>
  | <li><strong>Expires On:</strong> <span id="exp"></span></li>
  | <li><strong>Will Self Destruct:</strong> <span id="will_self_destruct"></span></li>
  | <li><strong>Raw:</strong> <span id="raw_uuid_url"></span></li>
  | </ul>
  | </div>
  | </div>
  |  
  | <div style="text-align: center">
  | <hr /><i>Copyright 2021 \| Developed with 💜 by <a href="https://hacked.is/">Hacked LLC</a></i>
  | </div>
  |  
  | </body>
  |  
  | </html>

<!--EndFragment-->
</body>
</html>
// Set page title
document.getElementsByTagName("title")
    .item(0)
    .innerText = `${document.getElementsByTagName("title")
    .item(0)
    .innerText} | View Paste`

// Get UUID
let uuid = window.location.href.split('/p/v/')[1];

// Get Key if exists
let isEncrypted = false;
if (window.location.href.split('#')[1]) {
    isEncrypted = true;
}

// Get Paste JSON
    fetch('/api/v1/paste?dataType=json&uuid='+uuid)
        .then(resp => resp.text())
        .then((json) => {
            document.getElementById("will_self_destruct").innerText = JSON.parse(json)['will_self_destruct'];
            document.getElementById("pub").innerText = JSON.parse(json)['published_on'];
            document.getElementById("exp").innerText = JSON.parse(json)['expired_on'];
            document.getElementById("raw_uuid_url").innerHTML = "<a href=\"" + window.location.href.split('/p/v/')[0] + "/api/v1/paste?dataType=text&uuid=" + uuid + "\">" + window.location.href.split('/p/v/')[0] + "/api/v1/paste?dataType=text&uuid=" + uuid + "</a>";
            document.getElementById("uuid").innerText = uuid;
        })
        .then(() => {
            // Get Paste Text
            fetch('/api/v1/paste?dataType=text&uuid='+uuid)
                .then(resp => resp.text())
                .then((text) => {
                    if (isEncrypted) {
                        let bytes  = CryptoJS.AES.decrypt(atob(text), atob(window.location.href.split('#')[1]));
                        let originalText = bytes.toString(CryptoJS.enc.Utf8);
                        document.getElementById("pasteContent").innerText = originalText;
                        document.getElementById("raw_uuid_url").innerHTML = "<a href=\"" + window.location.href.split('/p/v/')[0] + "/api/v1/paste?dataType=text&uuid=" + uuid.split("#")[0] + "\">" + window.location.href.split('/p/v/')[0] + "/api/v1/paste?dataType=text&uuid=" + uuid.split("#")[0] + "</a>";
                        document.getElementById("uuid").innerText = uuid.split("#")[0];
                        return originalText;
                    } else {
                        document.getElementById("pasteContent").innerText = text;
                        return text;
                    }
                })
                .then((text) => {
                    hljs.highlightAll();
                    hljs.initLineNumbersOnLoad({
                        singleLine: true,
                    });
                })
        })

Expected behavior I expect the content to be fetched, decrypted, hightlighted with highlightjs (this works great due to the usage of JavaScript promises) and finally for highlightjs line numbers to add a number to each line.

Screenshots image

Additional context Example URL https://paste.is/p/v/264ce2ae-c9c0-40f9-9863-61f1b0c3fd1b#NXZNeWVpM3lEV2xpNXZENldOOE5qaENUYlE5eHpoajJYTUJUblViQlZXWHY2dTJTYTFrWmVKeXZON0NHZ2haUQ==

wcoder commented 3 years ago

Thanks for the report!

  1. Broken highlighting: highlight.js detected the wrong language as less:

    Screen Shot 2021-05-11 at 12 53 08 AM
  2. Seems like the source code doesn't have line break symbols, could you please check?

    • you can decode line break as \n instead of < br >
  3. Also you can use another way to call the plugin:

const block = document.querySelector('code.hljs');
hljs.lineNumbersBlock(block);
elaine-jackson commented 3 years ago

HI thanks for replying quickly @wcoder . For the broken hilighting its unclear why the language was detected as less. I don't init hightlight until after the paste has been decrypted. I ensure this by using the promise based fetch API. This means that it will not init until the data has both been fetched and decrypted. So to answer your question about line breaks: On one page https://paste.is/p/v/2bcb2f5c-f795-466a-a128-b44bd0b06651 I turned off the encryption and the same bug occurs (see API response at https://paste.is/api/v1/paste?dataType=text&uuid=2bcb2f5c-f795-466a-a128-b44bd0b06651) as its a text file with new lines I believe line breaks are included as \n otherwise we wouldn't see the new lines? Beyond that is there any insight you could provide here if part of the issue lies in my code. It's admittedly challenging for my API serverto modify user input any as it's typically end to end encrypted to protect user privacy. Even when encrypted
are not added by the API or my JavaScript as far as I can tell.

Alternatively if you have insights on what a bug fix would ensue maybe I could contribute a pull request as your library is very helpful to my project.

elaine-jackson commented 3 years ago

One other note, it's possible that JavaScript changes the line breaks to
tags. Is there a way to turn off this behavior or replace every
element that was inserted into a \n again? I use the document.querySelector().innerText = to modify the #pasteContent element. This is needed because the API Server cannot modify an encrypted paste as not even I have access to the encryption keys. As a result when putting a user's content into the webpage I have to rely on JavaScript treating the text as text rather than HTML. Without it would be trivial for a user to put raw HTML and JavaScript into the page and perform a cross-site scripting attack.

A read of https://w3c.github.io/DOM-Parsing/#dfn-text doesn't mention the innerText adding <br> tags so it's unclear if this is undefined behavior and a browser implementation choice.

An unencrypted paste in VIM shows the line breaks.

image

elaine-jackson commented 3 years ago

On another note I did submit an issue to highlightjs project concerning the text being detected as LESS instead of C. https://github.com/highlightjs/highlight.js/issues/3184

wcoder commented 3 years ago

@irlcatgirl Please try to use innerHTML:

document.getElementById("pasteContent").innerHTML = textContent;

...

hljs.initHighlighting();
hljs.initLineNumbersOnLoad();

Works fine:

Screen Shot 2021-05-11 at 6 18 48 PM
elaine-jackson commented 3 years ago

So yes that works but it poses another problem (keep in mind the server cannot see let alone filter user input text). It's now unfiltered HTML / JS and results in a cross-site-scripting vulnerability. Any ideas on how to do about fixing it?

wcoder commented 3 years ago

Sure, you can use any HTML escaping library for this (on backend or frontend side).

elaine-jackson commented 3 years ago

So the following works for me

document.getElementById("pasteContent").innerHTML = originalText.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

a copy and paste of the https://owasp.org/www-community/xss-filter-evasion-cheatsheet does not trigger any alerts indicating its good enough.

elaine-jackson commented 3 years ago

Do you want me to close this issue since we found a solution or leave it open as a potential bug?

wcoder commented 3 years ago

Yes, it can be closed. Also, can be reopened when behaver will be wrong.