A global YouTube Player LP is smart — just don’t let it get hijacked

One of my fave life-simplifying moves is using a global Asset Download LP, rather than infinite separate LPs that only differ by the asset URL. You pass the asset URL as a query param (URL-encoded, of course):

https://pages.example.com/getasset?asset=/whitepapers/my%20asset.pdf

Then JS on the page automatically updates a button’s target to get that resource.[1]

Similarly, a single Media Player LP can grab the video ID from the query string:

https://pages.example.com/player?videoId=ABC_123z

Then the corresponding video is embedded using, for example, the YouTube IFRAME embed.

But in both cases you must make sure that your labor-saving dynamic page can’t be used by malicious actors to serve their assets via your domain.

Securing an Asset Download LP

To lock down to just your assets, simply hard-code the origin so it always  goes to your server:[2]

let thisURL = new URL(document.location.href);
let nextURL = new URL("https://assets.example.com");
nextURL.pathname = thisURL.searchParams.get("asset");

That’ll prevent someone from injecting their own domain into the asset param and using your page as a redirector. Sure, they can try it...

https://pages.example.com/getasset?asset=https://maliciousdude.example.net/gotcha.pdf

… but the final URL nextURL will be a harmless 404 on your site:

https://assets.example.com/https://maliciousdude.example.net/gotcha.net

Easy peasy.

Securing a Video Player LP

With video (YouTube here, but potentially other providers too) things are trickier. I noticed a client had a work-in-progress LP using this simple — all too simple! — code:

<main>
  <hgroup class="player-header">
    <h1>Video Gateway</h1>
  </hgroup>
  <hgroup class="video-header">
    <h2 class="video-title">[video title]</h2>
  </hgroup>
  <div id="ytplayer"></div>
</main>
<script>
  let query = new URL(document.location.href).searchParams,
      videoId = query.get("videoId"),
      videoTitle = query.get("videoTitle");
      
  let player;

  if (!videoId) {
    throw new Error("'videoId' param not found in query string");
  }
  
  document.querySelector(".video-header .video-title").textContent = videoTitle;

  function onYouTubePlayerAPIReady() {
    player = new YT.Player("ytplayer", {
      height: "360",
      width: "640",
      videoId: videoId
    });
  }
  
  let ytLib = document.createElement("script");
  ytLib.src = "https://www.youtube.com/player_api";
  document.head.append(ytLib);
</script>

Were it not for the security concern this code would be fine. You pass:

https://pages.example.com/player?videoId=ABC_123z&videoTitle=Logitech%20Sight…

And that video is smoothly embedded:

Player page lookin’ good

However! YouTube lets you embed any video using just the ID, and you cannot filter by channel. Which means a hacker could pass an ID of their choosing, and make it play on your site:

I hacked your player page

This could be, to put it mildly, very bad for brand protection.

Note restricting embedding isn’t a solution: that applies to your videos on other sites, which is the opposite case.[3]

The solution is pretty simple: pre-check the video ID to make sure it’s on your channel before activating the embed. For that, you use the YouTube Data API. (Other platforms have equivalent APIs but I only have so much room!)

Get a YouTube Data API key

The Data API may appear daunting, but we only need one method and it’s literally a single of line of JS (no giant JS SDK or anything). That method is Videos: List.

So provision an API key as below in the Developer Console. (This part may require help from whoever manages your Google account. If you’re using YouTube for business, you surely have such a person!)

YouTube Data API v3 key setup

Checking the channel

Videos: List returns a JSON response like this:

{
  "kind": "youtube#videoListResponse",
  "etag": "wM2qzhxik-iX5lWys2eCTZqF-pc",
  "items": [
    {
      "kind": "youtube#video",
      "etag": "KIyt999J_8kBp0U06w3_HdkgiHQ",
      "id": "DWq2eovHy_o",
      "snippet": {
        "publishedAt": "2022-11-28T00:31:56Z",
        "channelId": "UCzx3D4WEyAnK25Oujlnu3Fw",
        "title": "Does He Know",
        "description": "",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/DWq2eovHy_o/default.jpg",
            "width": 120,
            "height": 90
          }
        ...

So you can check the channelId against your channel’s unique ID, easily found in YouTube Studio:

Get the code

Here’s the updated code with brand protection:

<main>
  <hgroup class="player-header">
    <h1>Video Gateway</h1>
  </hgroup>
  <hgroup class="video-header">
    <h2 class="video-title">[video title]</h2>
  </hgroup>
  <div id="ytplayer"></div>
</main>
<script>
  let query = new URL(document.location.href).searchParams,
      videoId = query.get("videoId"),
      videoTitle = query.get("videoTitle");

  let player;
  let youTubeApiKey = "Pzw5V1nWZ4Uyc7j6zW6J23kTUzS14Nky";
  let ourChannelId = "UC_d7EY0NrcEM715w2n6yVZg";

  if (!videoId) {
    throw new Error("'videoId' param not found in query string");
  }

  fetch(`https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${youTubeApiKey}`)
    .then((respJ) => respJ.json())
    .then((resp) => {
       let isOurChannel = resp.items[0].snippet.channelId === ourChannelId;

       if(!isOurChannel) {
          throw new Error("Video is not in our channel");
       } else {
         document.querySelector(".video-header .video-title").textContent = resp.items[0].snippet.title;

         window.onYouTubeIframeAPIReady = function() {
           player = new YT.Player("ytplayer", {
             height: "360",
             width: "640",
             videoId: videoId
           });
         };      

         let ytLib = document.createElement("script");
         ytLib.src = "https://www.youtube.com/player_api";
         document.head.append(ytLib);
       }
    })
    .catch((e) => { 
      console.error(`YouTube prefetch error: ${e.message}`); 
    });
Notes

[1] Custom button text, asset friendly name, description, and other metadata can also be passed in the URL.

[2] Assuming all your assets are under the same domain. If they may be under a few different domains, that works too: pass the full URL in the query string (not just the pathname) and validate that the origin is in an array of allowedOrigins.

[3] Far as I know, there’s also no way to stop the YouTube site itself from playing a video, given the ID. Even trying to embed a specific @channel in the youtube.com URL doesn’t help: if the ID is found, it switches to that @channel.