Making Sense of Google's Web Fundamentals, Part 3

3/11/2019

This post is a continuation of part one and part two in a series that summarizes Google's Web Fundamentals.

Security

With so much news of security breaches lately, it's obvious why Google places a strong emphasis on security. This section lays out some of the major security protocols and best practices for the web.

Content Security Policy (CSP)

  • CSP is rooted in the Same-Origin Policy
  • However, websites can still be vulnerable to Cross Site Scripting (XSS)
  • Source whitelists:
    • By using the Content-Security-Policy HTTP header, you can control which scripts are executed from which domains. Example:
              
          Content-Security-Policy: script-src 'self' https://apis.google.com
              
            
    • The code above allows a site to execute scripts only from its own domain and "https://apis.google.com"
    • In addition to script-src, CSP provides some other directives:
      • base-uri restricts the URLs that can appear in a page's <base> element.
      • child-src lists the URLs for workers and embedded frame contents. For example: child-src https://youtube.com would enable embedding videos from YouTube but not from other origins.
      • connect-src limits the origins that you can connect to (via XHR, WebSockets, and EventSource).
      • font-src specifies the origins that can serve web fonts. Google's web fonts could be enabled via font-src https://themes.googleusercontent.com.
      • form-action lists valid endpoints for submission from <form> tags.
      • frame-ancestors specifies the sources that can embed the current page. This directive applies to <frame>, <iframe>, <embed>, and <applet> tags. This directive can't be used in <meta> tags and applies only to non-HTML resources.
      • frame-src was deprecated in level 2, but is restored in level 3. If not present it still falls back to child-src as before.
      • img-src defines the origins from which images can be loaded.
      • media-src restricts the origins allowed to deliver video and audio.
      • object-src allows control over Flash and other plugins.
      • plugin-types limits the kinds of plugins a page may invoke.
      • report-uri specifies a URL where a browser will send reports when a content security policy is violated. This directive can't be used in <meta> tags.
      • style-src is script-src's counterpart for stylesheets.
      • upgrade-insecure-requests instructs user agents to rewrite URL schemes, changing HTTP to HTTPS. This directive is for websites with large numbers of old URL's that need to be rewritten.
      • worker-src is a CSP Level 3 directive that restricts the URLs that may be loaded as a worker, shared worker, or service worker. As of July 2017, this directive has limited implementations.
      NOTE: please check the browser compatibility for these other attributes before using.
    • As an alternative to the Content-Security-Policy HTTP header, you can use an HTML meta tag:
                
          <meta http-equiv="Content-Security-Policy" content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'">
                
              
    • CSP, by default, prevents any inline or injected scripts, even ones added from your domain
      • As a best practice, you should avoid all inline and injected scripts
    • Check out the Real World Usage section of the CSP page on Web Fundamentals to see specific policies applied in the real world

Encrypting Data in Transit (HTTPS)

  • The benefits of HTTPS are authentication, data integrity, and secrecy
  • All websites should use HTTPS, even ones not handling sensitive data:
    • HTTPS may be a requirement in the future for all major search engines and browsers
    • Some cutting-edge features require the use of HTTPS.
  • Important Security Terminology:
    • Public/Private key pair:
      • Used as encryption/decryption key pairs in HTTPS
      • The public key is used to encrypt messages
      • The private key is used to decrypt messages
    • Certificate Authority (CA):
      • A CA is an organization that vouches for the mapping between public keys and public DNS names by cryptographically signing the public keys (X.509 certificate)
    • Certificate Signing Request (CSR): the data format used to request that a CA vouch for your key mappings
  • For details on how to enable HTTPS for a custom web server, check out Enabling HTTPS on Your Servers on Web Fundamentals.
  • Avoid mixed content by ensuring that all resources are requested via HTTPS

Help, I've been hacked

This section provides some general info on identifying and resolving threats and attacks on a website.

  • How do I know if my site is hacked?
  • Top website hacks:
    • Compromised passwords:
    • Missing security updates:
      • Older versions of software related to a site may have security vulnerabilities. This software could be the web server, the core CMS or other add-ons (such as NPM packages)
      • To prevent these vulnerabilities:
        • Update this software frequently
        • Always conduct a security review of any 3rd-party software used by the site
    • Social Engineering
      • When hackers trick people into revealing sensitive information. A major example of this is "phishing"
      • To prevent social engineering, educate and train your users, if possible
  • Security policy holes
    • Allowing users to create weak passwords
    • Giving administrative access to users who don’t require it
    • Not enabling HTTPS on your site and allowing users to sign in using HTTP
    • Allowing file uploads from unauthenticated users, or with no type checking
  • Build a support team
    1. Tell your web host that your site has been compromised. (Often the web host company or department can help.)
    2. Reach out to online communities such as StopBadWare.org and Google Webmaster Central
    3. Bring in a trusted security expert
  • Quarantine your site
    1. Take your site offline by contacting your web host. (Returning 4xx or 5xx status codes and disallowing with robots.txt are not sufficient.)
    2. Perform thorough user account management
      • Identify any suspicious users
      • Change the passwords for all site users and accounts
  • Assess spam damage
  • Additional security checks:

Base Technologies

This section covers some core Javascript and DOM APIs, such as async functions, promises, service workers, and web components.

Web Components

Web Components comprise a set of Javascript APIs and HTML conventions that allow developers to create encapsulated and customizable components that can be included with any web page.

Before we dig into Web Components, some ancillary points are worth mentioning:

  • While they cannot be compared directly, Web Components and frameworks/libraries, like React, compete for the same goal of providing reusable components. Developers often have to make the difficult determination of when to use either or both of these technologies.
  • Browser support for web components is still spotty. If authoring web components, always use a polyfill like LitElement to reconcile the differences between browsers.

Basic Web Component example:

(Use the code above as a reference for the "Custom Elements v1" and "Shadow DOM v1" sections that follow.)

Custom Elements v1

  • The Custom Elements API allows developers to create new HTML tags with associated behavior
  • This API is the foundation of Web Components
  • Defining a custom element:
    1. Create a class that extends HTMLElement or another native element such as HTMLButtonElement (see code sample above)
    2. Call window.customElements.define('custom-cls-from-above', CustomClassFromAbove) (see code sample above)
    3. Example usage: <custom-cls-from-above></custom-cls-from-above> (see code sample above)
  • Defining an element's JavaScript API:
    • Add any number of get/set properties
    • Map these properties to attributes by calling this.setAttribute('attrName', val) and this.getAttribute('attrName')
  • Rules on creating custom elements:
    1. The name of a custom element must contain one or more dashes (-)
    2. You can't register the same tag more than once
    3. Custom elements cannot be self-closing
  • Custom element reactions (aka "lifecycle hooks"):
    • constructor: called when an instance of the element is created. Use for initial state, event listeners, or creating Shadow DOM
    • connectedCallback: called when an element is inserted into the DOM
    • disconnectedCallback: called when an element is removed from the DOM
    • attributeChangedCallback: Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial values when an element is created by the parser or upgraded. Note: only attributes listed in the observedAttributes property will receive this callback
  • Element upgrades:
    • Progressively enhanced HTML
      • Custom elements can be used before they have been registered with window.customElements.define(...)
      • Before being registered, the browser will render them as an HTMLUnknownElement
      • Use the following code to handle when a custom element gets defined:
                      
            window.customElements.whenDefined('app-drawer').then(() => {
              console.log('app-drawer defined')
            })
                      
                    
  • Element-defined content
    • You can use an HTML template element to define markup for your component
    • You can then append this template to the component's shadow DOM by calling this.attachShadow({mode: 'open'}) (see code sample above)
  • Styling a custom element
    • Users can override a custom element's shadow DOM styling through custom CSS. (See "user-defined styling" in the code sample above)
    • The :defined CSS pseudo selector is useful for styling before the element has been registered
  • Extending elements
    • You can extend another custom element as well:
                
          class FancyDrawer extends AppDrawer {...}
                
              
    • Also, you can extend native html elements:
                
          class FancyButton extends HTMLButtonElement {...}
                
              

Shadow DOM v1

  • Because the DOM and CSS have traditionally relied on globals, encapsulation can be a challenge.
  • The Shadow DOM provides encapsulation to web components through an isolated DOM and scoped CSS
  • Alternatives to the shadow DOM are BEM, CSS Modules, and CSS in JS
  • You create a shadow DOM by calling .attachShadow(options) on an element (see code sample above)
  • Slots:
    • The slot element allows the consumer to inject custom content into the component (see code sample above)
    • You can provide multiple slots by specifying a name attribute (see code sample above)
  • Light DOM refers to the HTML the consumer includes in slot elements
  • Shadow DOM refers to the HTML that is part of the component itself
  • The result of combining the light and shadow DOM elements is a flattened DOM tree. You can see this tree in Chrome DevTools under the #shadow-root of the component
  • Component-defined styles:
    • CSS selectors used inside the shadow DOM apply locally to your component. Therefore, you can specify class names/ids/names without worrying about collisions from the outer scope
    • A style tag within the shadow root of the component defines the local CSS (see code sample above)
    • The :host selector refers to the component root. :host can also use other pseudo selectors like so: :host(:hover) {...} (see code sample above)
    • The :host-context selector refers to any of the parent elements of the component (see code sample above)
    • The ::slotted selector refers to elements within a slot element (see code sample above)
    • To style a component from the outside, you can use its custom element name as the selector. These outer styles take priority over the ones defined in the shadow DOM (see code sample above)
    • Styling hooks can be created using custom CSS properties. You create these hooks by using the CSS var keyword (see code sample above)
  • Advanced topics:
    • Closed components: you can create components whose DOM cannot be accessed by calling attachShadow({mode: 'closed'}), but this is a bad practice for many reasons
    • You can trigger custom events from a component by calling dispatchEvent(new Event('some-event', {bubbles: true, composed: true})) (see code sample above)
    • Handling focus within elements in a component requires a little extra work
  • Tips & tricks:
    • Use the CSS property contain: content; for better rendering performance (see code sample above)
    • Use the CSS property all: initial; to reset all styles inherited from the outer scope (see code sample above)
  • History & browser support
    • Use a polyfill such as ShadyDOM to ensure cross-browser support of your components
  • Check out additional best practices and examples for web components on Web Fundamentals

Javascript

Below is some general info on async functions, promises, and service workers:

Async functions

The async/await keywords provide syntactic sugar on top of promises and can help to simplify the code. Example:

Before async/await:

  
  function logFetch(url) {
  return fetch(url)
    .then(response => response.text())
    .then(text => {
      console.log(text);
    }).catch(err => {
      console.error('fetch failed', err);
    });
  }
  

With async/await:

  
  async function logFetch(url) {
    try {
      const response = await fetch(url);
      console.log(await response.text());
    }
    catch (err) {
      console.log('fetch failed', err);
    }
  }
  

Promises

Promises are a task-based API and provide an alternative to callbacks. Example:

  
  promise.then(function(result) {
    console.log(result); // "Stuff worked!"
  }, function(err) {
    console.log(err); // Error: "It broke"
  });
  

Service Workers

The Service Worker API allows a web application to intercept network traffic and take appropriate action. They are essential for many of the features that Progressive Web Apps can provide, such as offline support and push notifications. Also, by adding offline caching capabilities, service workers can improve the loading performance of your page by limiting the number of network requests for resources like CSS, Javascript, and images.

Basic service worker code sample:

The HTML code:

  
  <!DOCTYPE html>
  An image will appear here in 3 seconds:
  <script>
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW registered!', reg))
      .catch(err => console.log('Boo!', err));

    setTimeout(() => {
      const img = new Image();
      img.src = '/dog.svg';
      document.body.appendChild(img);
    }, 3000);
  </script>
  

The Javascript code (sw.js):

  
    const expectedCaches = ['static-v2'];

    self.addEventListener('install', event => {
      console.log('V2 installing…');
    
      // cache a horse SVG into a new cache, static-v2
      event.waitUntil(
        caches.open('static-v2').then(cache => cache.add('/horse.svg'))
      );
    });
    
    self.addEventListener('activate', event => {
      // delete any caches that aren't in expectedCaches
      // which will get rid of static-v1
      event.waitUntil(
        caches.keys().then(keys => Promise.all(
          keys.map(key => {
            if (!expectedCaches.includes(key)) {
              return caches.delete(key);
            }
          })
        )).then(() => {
          console.log('V2 now ready to handle fetches!');
        })
      );
    });
    
    self.addEventListener('fetch', event => {
      const url = new URL(event.request.url);
    
      // serve the horse SVG from the cache if the request is
      // same-origin and the path is '/dog.svg'
      if (url.origin == location.origin && url.pathname == '/dog.svg') {
        event.respondWith(caches.match('/horse.svg'));
      }
    });
  

(You can use the sample above as a reference for the info below.)

  • Service workers supersede an older technology called AppCache
  • Like all Javascript workers, service workers cannot access the DOM
  • They are terminated when not in use and restarted when next needed. That means you cannot rely on global state within service workers.
  • A library like Google Workbox can simplify development
  • Service workers now have fairly broad support
  • Service workers can be problematic if multiple browser tabs are open for the same web app
  • Service workers require HTTPS
  • The service worker life cycle:
    • Register
      • Registration happens by calling navigator.serviceWorker.register('/sw.js') (see sample code above)
      • Service workers are registered at the root of your application by default. If you registered one from a subdirectory like "/foo/sw.js", it would only control pages under "/foo/". It's best to always register from the root. Also, you should never change the URL of the service worker once it is registered.
      • If the script fails to download, parse or throws an error, the browser will discard the service worker
    • Install
      • The install event is the first event fired for a service worker
      • install only fires once
      • If the service worker code changes, a new instance is created, triggering the install event again. In this case, there are now two service workers running --the current one and the new one. When the current worker is finished controlling pages, it gets replaced by the new one. You can override this behavior by calling self.skipWaiting(). However, you won't see any changes until you close the page and open it again in a new tab.
      • install is where you should load resources into the cache. Calling event.waitUntil(...) forces the service worker to stay alive until the caching promise returns (see sample code above)
    • Activate
      • When the activate event fires, the service worker is ready to control the page
      • activate is where you should handle events like fetch to serve requests from the cache (see sample code above)
      • By default, the requests won't be served from cache right after the first load, but immediately after, on the second load. You can override this behavior by calling clients.claim()
  • Using Chrome DevTools:
    • DevTools can make development easier, especially when changing the service worker code frequently
    • You can find service worker tools in the Application tab
    • Select Update on reload to force a service worker update when refreshing the page
    • Click skipWaiting to force a new service worker to take effect right after installation

Wow, that was a long journey! I hope you find this summary useful. And, feel free to contact me with any questions or suggestions.