Slide 1

Slide 1 text

THE ANNOYING SITE aka "The Power of the Web Platform" It's 2018. The web platform offers dozens of powerful, native-like APIs. What if we used this newfound power for evil? What kind of terrible UX could we create if our goal was to build the worst web page in the world?

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

BITMIDI.COM

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

while (true) { alert('Help! I\'m caught in a loop and can\'t get out!') }

Slide 6

Slide 6 text

Source: xkcd.com

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

▸ 90's - Web is the wild west ▸ 00's - Security is important ▸ 10's - Powerful features are important

Slide 10

Slide 10 text

THE WEB PLATFORM HAS BECOME REALLY POWERFUL

Slide 11

Slide 11 text

THE WEB PLATFORM HAS LOTS OF ANCIENT APIS

Slide 12

Slide 12 text

Every system has two sets of rules: The rules as they are intended or commonly perceived, and the actual rules ("reality"). — Paul Buchheit

Slide 13

Slide 13 text

HACKING TAKING ADVANTAGE OF THE GAP BETWEEN THE WRITTEN RULES AND THE ACTUAL RULES

Slide 14

Slide 14 text

▸ and (Java Web Start, Adobe Flash) ▸ ▸ ▸ ▸ String.prototype.big ▸ String.prototype.quote

Slide 15

Slide 15 text

'Hello world'.big() // Returns 'Hello world' 'Hello world'.quote() // Returns '"Hello world"'

Slide 16

Slide 16 text

MOVE A WINDOW AROUND ▸ window.moveTo() ▸ window.moveBy() ▸ window.resizeTo() ▸ window.resizeBy()

Slide 17

Slide 17 text

SAME ORIGIN POLICY

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

OPEN A WINDOW const win = window.open('', '', 'width=100,height=100')

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

OPEN A WINDOW & MOVE IT AROUND const win = window.open('', '', 'width=100,height=100') win.moveTo(10, 10) win.resizeTo(200, 200)

Slide 22

Slide 22 text

"USER INITIATED" EVENT HANDLER document.addEventListener('click', () => { const win = window.open('', '', 'width=100,height=100') win.moveTo(10, 10) win.resizeTo(200, 200) })

Slide 23

Slide 23 text

NOW, ADD THE FUN PART let i = 0 setInterval(() => { win.moveTo(i, i) i = (i + 5) % 200 }, 100)

Slide 24

Slide 24 text

DEMO

Slide 25

Slide 25 text

INTERCEPT ALL USER-INITIATED EVENTS function interceptUserInput (onInput) { document.body.addEventListener('touchstart', onInput, { passive: false }) document.body.addEventListener('mousedown', onInput) document.body.addEventListener('mouseup', onInput) document.body.addEventListener('click', onInput) document.body.addEventListener('keydown', onInput) document.body.addEventListener('keyup', onInput) document.body.addEventListener('keypress', onInput) }

Slide 26

Slide 26 text

OPEN CHILD WINDOW function openWindow () { const { x, y } = getRandomCoords() const opts = `width=${WIN_WIDTH},height=${WIN_HEIGHT},left=${x},top=${y}` const win = window.open(window.location.pathname, '', opts) // New windows may be blocked by the popup blocker if (!win) return wins.push(win) } interceptUserInput(event => { event.preventDefault() event.stopPropagation() openWindow() })

Slide 27

Slide 27 text

FOCUS ALL WINDOWS ON CLICK function focusWindows () { wins.forEach(win => { if (!win.closed) win.focus() }) }

Slide 28

Slide 28 text

BOUNCE WINDOW OFF THE SCREEN EDGES function moveWindowBounce () { let vx = VELOCITY * (Math.random() > 0.5 ? 1 : -1) let vy = VELOCITY * (Math.random() > 0.5 ? 1 : -1) window.setInterval(() => { const x = window.screenX const y = window.screenY const width = window.outerWidth const height = window.outerHeight if (x < MARGIN) vx = Math.abs(vx) if (x + width > SCREEN_WIDTH - MARGIN) vx = -1 * Math.abs(vx) if (y < MARGIN + 20) vy = Math.abs(vy) if (y + height > SCREEN_HEIGHT - MARGIN) vy = -1 * Math.abs(vy) window.moveBy(vx, vy) }, TICK_LENGTH) }

Slide 29

Slide 29 text

PLAY RANDOM VIDEO IN THE WINDOW const VIDEOS = [ 'albundy.mp4', 'badger.mp4', 'cat.mp4', 'hasan.mp4', 'heman.mp4', 'jozin.mp4', 'nyan.mp4', 'rickroll.mp4', 'space.mp4', 'trolol.mp4' ] function startVideo () { const video = document.createElement('video') video.src = getRandomArrayEntry(VIDEOS) video.autoplay = true video.loop = true video.style = 'width: 100%; height: 100%;' document.body.appendChild(video) }

Slide 30

Slide 30 text

CAN WE DO MORE?

Slide 31

Slide 31 text

YES.

Slide 32

Slide 32 text

PERMISSION MODEL ▸ Always works ▸ Always works, but HTTPS protocol required ▸ User interaction required ▸ User permission required

Slide 33

Slide 33 text

COPY SPAM TO CLIPBOARD const ART = [ ` ░░▓▓░░░░░░░░▓▓░░ ░▓▒▒▓░░░░░░▓▒▒▓░ ░▓▒▒▒▓░░░░▓▒▒▒▓░ ░▓▒▒▒▒▓▓▓▓▒▒▒▒▓░ ░▓▒▒▒▒▒▒▒▒▒▒▒▒▒▓ ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ ▓▒▒▒░▓▒▒▒▒▒░▓▒▒▓ ▓▒▒▒▓▓▒▒▒▓▒▓▓▒▒▓ ▓▒░░▒▒▒▒▒▒▒▒▒░░▓ ▓▒░░▒▓▒▒▓▒▒▓▒░░▓ ░▓▒▒▒▓▓▓▓▓▓▓▒▒▓░ ░░▓▒▒▒▒▒▒▒▒▒▒▓░░ ░░░▓▓▓▓▓▓▓▓▓▓░░░ `, ... ] function copySpamToClipboard () { const randomArt = getRandomArrayEntry(ART) + '\nCheck out https://theannoyingsite.com' clipboardCopy(randomArt) }

Slide 34

Slide 34 text

SPEAK TO THE USER const PHRASES = [ 'The wheels on the bus go round and round, round and round, round and round', 'I like fuzzy kittycats, warm eyes, and pretending household appliances have feelings', 'I\'ve never seen the inside of my own mouth because it scares me to death', 'eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo eyo' 'Dibidi ba didi dou dou, Di ba didi dou, Didi didldildidldidl houdihoudi dey dou', 'hee haw hee haw hee haw hee haw hee haw hee haw hee haw hee haw hee haw hee haw hee haw', 'abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak', ] function speak (phrase) { if (phrase == null) phrase = getRandomArrayEntry(PHRASES) window.speechSynthesis.speak(new window.SpeechSynthesisUtterance(phrase)) } interceptUserInput(speak) window.addEventListener('beforeunload', () => speak('Please don\'t go!')) Source: frankmtaylor.com

Slide 35

Slide 35 text

CONFIRM PAGE UNLOAD function confirmPageUnload () { window.addEventListener('beforeunload', event => { event.returnValue = true }) }

Slide 36

Slide 36 text

REGISTER PROTOCOL HANDLERS function registerProtocolHandlers () { const protocolWhitelist = [ 'bitcoin', 'geo', 'im', 'irc', 'ircs', 'magnet', 'mailto', 'mms', 'news', 'ircs', 'nntp', 'sip', 'sms', 'smsto', 'ssh', 'tel', 'urn', 'webcal', 'wtai', 'xmpp' ] const handlerUrl = window.location.href + '/url=%s' protocolWhitelist.forEach(proto => { navigator.registerProtocolHandler(proto, handlerUrl, 'The Annoying Site') }) }

Slide 37

Slide 37 text

REQUEST CAMERA AND MIC function requestCameraAndMic () { navigator.mediaDevices.enumerateDevices().then(devices => { const cameras = devices.filter((device) => device.kind === 'videoinput') if (cameras.length === 0) return const camera = cameras[cameras.length - 1] navigator.mediaDevices.getUserMedia({ deviceId: camera.deviceId, facingMode: ['user', 'environment'], audio: true, video: true }).then(stream => { const track = stream.getVideoTracks()[0] const imageCapture = new window.ImageCapture(track) imageCapture.getPhotoCapabilities().then(() => { // Let there be light! track.applyConstraints({ advanced: [{torch: true}] }) }, () => { /* No torch on this device */ }) }, () => { /* ignore errors */ }) }) }

Slide 38

Slide 38 text

START VIBRATE INTERVAL function startVibrateInterval () { setInterval(() => { const duration = Math.floor(Math.random() * 600) window.navigator.vibrate(duration) }, 1000) }

Slide 39

Slide 39 text

START SAFARI PICTURE-IN-PICTURE function startInvisiblePictureInPictureVideo () { const video = document.createElement('video') video.src = getRandomArrayEntry(VIDEOS) video.autoplay = true video.loop = true video.muted = true video.style = HIDDEN_STYLE document.body.appendChild(video) } function enablePictureInPicture () { const video = document.querySelector('video') if (video.webkitSetPresentationMode) { video.muted = false video.webkitSetPresentationMode('picture-in-picture') } }

Slide 40

Slide 40 text

HIDE THE CURSOR function hideCursor () { document.querySelector('html').style = 'cursor: none;' }

Slide 41

Slide 41 text

TRIGGER A FILE DOWNLOAD const FILE_DOWNLOADS = [ 'cat-blue-eyes.jpg', 'cat-ceiling.jpg', 'cat-crosseyes.jpg', 'cat-cute.jpg', 'cat-hover.jpg', 'cat-marshmellows.jpg', 'cat-small-face.jpg', 'cat-smirk.jpg' ] function triggerFileDownload () { const fileName = getRandomArrayEntry(FILE_DOWNLOADS) const a = document.createElement('a') a.href = fileName a.download = fileName a.click() }

Slide 42

Slide 42 text

SHOW A MODAL TO PREVENT WINDOW CLOSE function showModal () { if (Math.random() < 0.5) { showAlert() } else { window.print() } } function showAlert () { const randomArt = getRandomArrayEntry(ART) const longAlertText = Array(200).join(randomArt) window.alert(longAlertText) }

Slide 43

Slide 43 text

SHOW A MODAL REGULARLY function startAlertInterval () { setInterval(showModal, 30000) }

Slide 44

Slide 44 text

FULLSCREEN THE BROWSER function requestFullscreen () { const requestFullscreen = Element.prototype.requestFullscreen || Element.prototype.webkitRequestFullscreen || Element.prototype.mozRequestFullScreen || Element.prototype.msRequestFullscreen requestFullscreen.call(document.body) }

Slide 45

Slide 45 text

LOG THE USER OUT OF POPULAR SITES (PART 1) const LOGOUT_SITES = { 'AOL': ['GET', 'https://my.screenname.aol.com/_cqr/logout/mcLogout.psp?sitedomain=startpage.aol.com&authLev=0&lang=en&locale=us'], 'AOL 2': ['GET', 'https://api.screenname.aol.com/auth/logout?state=snslogout&r=' + Math.random()], 'Amazon': ['GET', 'https://www.amazon.com/gp/flex/sign-out.html?action=sign-out'], 'Blogger': ['GET', 'https://www.blogger.com/logout.g'], 'Delicious': ['GET', 'https://www.delicious.com/logout'], // works! 'DeviantART': ['POST', 'https://www.deviantart.com/users/logout'], 'DreamHost': ['GET', 'https://panel.dreamhost.com/index.cgi?Nscmd=Nlogout'], 'Dropbox': ['GET', 'https://www.dropbox.com/logout'], 'eBay': ['GET', 'https://signin.ebay.com/ws/eBayISAPI.dll?SignIn'], 'Gandi': ['GET', 'https://www.gandi.net/login/out'], 'GitHub': ['GET', 'https://github.com/logout'], 'GMail': ['GET', 'https://mail.google.com/mail/?logout'], 'Google': ['GET', 'https://www.google.com/accounts/Logout'], // works! 'Hulu': ['GET', 'https://secure.hulu.com/logout'], 'Instapaper': ['GET', 'https://www.instapaper.com/user/logout'], 'Linode': ['GET', 'https://manager.linode.com/session/logout'], 'LiveJournal': ['POST', 'https://www.livejournal.com/logout.bml', {'action:killall': '1'}], 'MySpace': ['GET', 'https://www.myspace.com/index.cfm?fuseaction=signout'], ... } Source: superlogout.com

Slide 46

Slide 46 text

LOG THE USER OUT OF POPULAR SITES (PART 2) function superLogout () { for (let name in LOGOUT_SITES) { const method = LOGOUT_SITES[name][0] const url = LOGOUT_SITES[name][1] const params = LOGOUT_SITES[name][2] || {} if (method === 'GET') { get(url) } else { post(url, params) } const div = document.createElement('div') div.innerText = `Logging you out from ${name}...` const logoutMessages = document.querySelector('.logout-messages') logoutMessages.appendChild(div) } } Source: superlogout.com

Slide 47

Slide 47 text

DISABLE THE BACK BUTTON function blockBackButton () { window.addEventListener('popstate', () => window.history.forward()) }

Slide 48

Slide 48 text

FILL THE HISTORY WITH EXTRA ENTRIES function fillHistory () { for (let i = 1; i < 20; i++) { window.history.pushState({}, '', window.location.pathname + '?q=' + i) } // Set location back to the initial location, so user does not notice window.history.pushState({}, '', window.location.pathname) }

Slide 49

Slide 49 text

DO EMBARRASSING SEARCHES (PART 1) const SEARCHES = [ 'where should i bury the body', 'why does my eye twitch', 'why is my poop green', 'why do i feel so empty', 'why do i always feel hungry', 'why do i always have diarrhea', 'why does my anus itch', 'why does my belly button smell', 'why does my cat attack me', 'why does my dog eat poop', 'why does my fart smell so bad', 'why does my mom hate me', 'why does my pee smell bad', 'why does my poop float', 'proof that the earth is flat' ]

Slide 50

Slide 50 text

DO EMBARRASSING SEARCHES (PART 2) function setupSearchWindow (win) { if (!win) return const search = getRandomArrayEntry(SEARCHES) win.window.location = 'https://www.bing.com/search?q=' + encodeURIComponent(search) let searchIndex = 1 let interval = setInterval(() => { if (searchIndex >= SEARCHES.length) { clearInterval(interval) win.window.location = window.location.pathname return } if (win.closed) { clearInterval(interval) onCloseWindow(win) return } win.window.location = window.location.pathname setTimeout(() => { const { x, y } = getRandomCoords() win.moveTo(x, y) win.window.location = 'https://www.bing.com/search?q=' + encodeURIComponent(SEARCHES[searchIndex]) searchIndex += 1 }, 500) }, 2500) }

Slide 51

Slide 51 text

TAKEOVER OPENER WINDOW (PART 1) Link

Slide 52

Slide 52 text

TAKEOVER OPENER WINDOW (PART 2) function isParentSameOrigin () { try { // May throw an exception if `window.opener` is on another origin return window.opener.location.origin === window.location.origin } catch (err) { return false } } function attemptToTakeoverReferrerWindow () { if (isParentWindow && !isParentSameOrigin()) { window.opener.location = `${window.location.origin}/?child=true` } }

Slide 53

Slide 53 text

ALWAYS USE REL='NOOPENER' Link

Slide 54

Slide 54 text

DEMO

Slide 55

Slide 55 text

THEANNOYINGSITE.COM

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

MORE DEMOS

Slide 58

Slide 58 text

Thanks! @FEROSS • FEROSS.ORG