Tech

Every line (of the code) matters

How many vulnerabilities can three lines of code introduce

You have started working on a new project. And from time to time when you are reading the code in the repository you have this (hard to define) feeling. Feeling that something is wrong. So you are going deeper into the code and start playing with it.

Let’s have a look at the following piece of code that I have discovered. It was like this:

router.get('/image/:url', (req, res) => {
  request
    .get(req.params.url)
    .pipe(res);
});

This code had one job: download image(hosted on another website) and sent it to the user on our domain (myDomain.com).

For example, when you would go to the URL:

https://myDomain.com/image/https://another.domain/thepicture.jpg

The users would see thepicture.jpg hosted on another.domain as hosted on your myDomain.com domain.

I should mention that: there are two assumptions: first is that request library only allows you to use HTTP and HTTPS links, and second: there is caching layer between the code and client 🙂

The question arrives: What is wrong with this code? It is doing the job requested by the Product Owner / Client.

Yes. It does. It is doing even more. This code is script kiddies heaven (and for real bad actors too).

Let see what vulnerabilities it opens.

  • You could put in the place of image parameter whatever you want. You could put your malicious script URL. Just prepare simple malicious HTML that contains some JS that (for example) would send cookies to your servers and then redirect users to for example image. Just to keep it quiet.
    Now send your prepared URL to the users writing that the website is not working correctly. And they must check it! Before they become suspicious(if they ever would be) you will have their cookies and other data stored on myDomain.com.
  • Use this service as your’s files proxy – for serving viruses or similar things. Just imagine how your’s users are going to react when your website is going to be added to the blacklist with malicious domains.
    That is what they are going to see:
  • What is worst – you could put in the place of image param resources that are accessible only from your network – for example (intra.my.company/importantDoc.pdf). Files, documents, code – you name it. When the hacker is going to guess the local domain or IP address with data crucial to your business you have a data breach. And the GDPR on your back.

What could you do to prevent that or what should you do if you need to do that kind of a proxy?

  • Never ever allow wildcard redirects/content proxying. Always use some hash/checksum when generating URL. So users cannot put their (malicious) parameters.
  • Always check the type of content that you are downloading. If you want to proxy images – download only files that are of type image. And send header with content-type. The browser should follow the header value when trying to open the web address.
  • Isolate your servers from your corporate network. If you need to call some service in VPN – use non-standard port and/or proxy service that will monitor and allow for only a small number of types of requests.

Example of the code that should not have loopholes

const crypto = require('./crypto'); // library that can encrypt and decrypt some text
const validator = require('./validator'); // library that can perform validation on stream

const app = express();

const IMAGE_FROM_TRUSTED_SOURCE = 'https://trusted.domain/thepicture.jpg';

router.get('/', (req, res) => {
  const {hash, cryptedUrl} = crypto.encrypt(IMAGE_FROM_TRUSTED_SOURCE);
  const imageUrl = `/image/v1/${hash}/${cryptedUrl}`;

  res.end(`<img src="${imageUrl}" alt="Image that can be trusted" />`);
});

router.get('/image/v1/:hash/:cryptedUrl', (req, res) => {
  const cryptedUrl = request.params.cryptedUrl;
  const hash = request.params.hash;

  try {
    const url = crypto.decrypt(cryptedUrl, hash); // crypto has it's own secret/salt that is combined with "public" secret(hash)

    request
      .get(url)
      .pipe(validator.validateContentType(['image/jpg', 'image/png']))
      .pipe(res);
  } catch (error) {
    console.error('Someone is probably doing something naughty', error);

    res.end();
  }
});

As we can see when we want to send an image link to the user firstly we must encrypt the URL. It should be done by the commonly used library – for example, Node.JS has Crypto.

When the user wants to open your URL no malicious operation could be performed on URL because later the decryption is going to fail. The encryption itself should use besides key that is visible to the user(called in the code hash) also a private secret key that will never be known to the public.

So now we are safe and sound!