diff --git a/.gitignore b/.gitignore index 49c1963d..2f136b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,6 @@ data/videos/* !www/videos/screen-saver.ogv !www/videos/screen-saver.webm -data/browserfest-submissions.json -data/browserfest-thumbnails/* -!data/browserfest-thumbnails/.gitkeep - # ignoring tutorial data for now, to avoid bloating things up www/tutorials/* !www/tutorials/list.json diff --git a/data/browserfest-thumbnails/.gitkeep b/data/browserfest-thumbnails/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/my_modules/github.js b/my_modules/github.js index cc648872..06ce6878 100644 --- a/my_modules/github.js +++ b/my_modules/github.js @@ -1,4 +1,3 @@ -const axios = require('axios') const triplesec = require('triplesec') const { Octokit } = require('@octokit/core') const express = require('express') @@ -104,17 +103,25 @@ function reWriteCSSPaths (req, data) { // HACK!!! return str } -router.get('/api/github/proxy', (req, res) => { +router.get('/api/github/proxy', async (req, res) => { + if (!req.query.url) return res.status(400).json({ error: 'missing url param' }) + // fix single-slash typo that can come from the redbird proxy const url = req.query.url.replace('https:/raw', 'https://raw') - // HACK: the purpose of this proxy to get around this issue: + // only allow raw.githubusercontent.com — no open SSRF + let parsed + try { parsed = new URL(url) } catch { return res.status(400).json({ error: 'invalid url' }) } + if (parsed.hostname !== 'raw.githubusercontent.com') { + return res.status(403).json({ error: 'host not allowed' }) + } + // HACK: proxy exists to get around MIME-type mismatch issue with raw.githubusercontent.com // https://stackoverflow.com/questions/40728554/resource-blocked-due-to-mime-type-mismatch-x-content-type-options-nosniff/41309463#41309463 - axios.get(url, { responseType: 'arraybuffer' }) - .then(r => { - if (req.query.url.includes('.css')) { - res.end(reWriteCSSPaths(req, r.data)) - } else res.end(r.data) - }) - .catch(err => console.log(err)) + try { + const r = await fetch(url) + const buf = await r.arrayBuffer() + if (req.query.url.includes('.css')) { + res.end(reWriteCSSPaths(req, Buffer.from(buf))) + } else res.end(Buffer.from(buf)) + } catch (err) { console.log(err) } }) // ~ * ~ . _ . ~ * ~ . _ . ~ * ~ . _ . ~ * ~ . _ . ~ * Auth Token @@ -141,11 +148,10 @@ router.get('/user/signin/callback', (req, res) => { const root = 'https://github.com/login/oauth/access_token' const id = `client_id=${process.env.GITHUB_CLIENT_ID}` const sec = `client_secret=${process.env.GITHUB_CLIENT_SECRET}` - axios.post(`${root}?${id}&${sec}&${code}`, { // ask GitHub for Auth Token - method: 'post', + fetch(`${root}?${id}&${sec}&${code}`, { // ask GitHub for Auth Token + method: 'POST', headers: { Accept: 'application/json' } - }).then(response => { - const token = response.data + }).then(response => response.text()).then(token => { const ermsg = '◕ ︵ ◕ oh no! looks like something went wrong with GitHub' if (token.indexOf('error') === 0) return res.send(ermsg) // ...assuming we don't get an error back, let's encrypt the token diff --git a/my_modules/routes.js b/my_modules/routes.js index 577c289a..9cb28c10 100644 --- a/my_modules/routes.js +++ b/my_modules/routes.js @@ -5,9 +5,7 @@ const fs = require('fs') const { promisify } = require('util') const readdir = promisify(fs.readdir) const stat = promisify(fs.stat) -const exec = require('child_process').exec const utils = require('./utils.js') -const axios = require('axios') const os = require('os') // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // @@ -45,11 +43,16 @@ otherAssets.forEach(dir => { aliasRoutes.forEach(dep => { if (dep.url.includes('*')) { // for routes with wildcards router.get(dep.url, (req, res) => { // req.params[0] contains the wildcard path - const filePath = path.join(__dirname, dep.loc, req.params[0]) - res.sendFile(filePath) + res.setHeader('Access-Control-Allow-Origin', '*') + res.sendFile(req.params[0], { root: path.join(__dirname, dep.loc) }, (err) => { + if (err) res.status(404).end() + }) }) } else { // for exact routes - router.get(dep.url, (req, res) => res.sendFile(path.join(__dirname, dep.loc))) + router.get(dep.url, (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.sendFile(path.join(__dirname, dep.loc)) + }) } }) @@ -59,8 +62,10 @@ router.get('/tutorials/*', (req, res, next) => { const host = req.hostname // any subdomains (ex dev.netnet) should load tutorials from production server if (host.endsWith('.netnet.studio') && !req.originalUrl.endsWith('/list.json')) { - const filePath = path.join(__dirname, '../../netnet.studio/www', req.originalUrl) - return res.sendFile(filePath) + const root = path.resolve(__dirname, '../../netnet.studio/www') + return res.sendFile(req.params[0], { root }, (err) => { + if (err) res.status(404).end() + }) } // NOTE: v2 && v3 have a modified version of this route which returns the old // tutorials, stored in ../../netnet.studio-v3/www @@ -68,6 +73,8 @@ router.get('/tutorials/*', (req, res, next) => { }) // directory listing for /templates/* +const escapeHtml = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])) + const templateListPage = (rel, upLink, rows) => { const arr = rel.split('/').filter(s => s !== '') arr.splice(0, 2) @@ -76,7 +83,7 @@ const templateListPage = (rel, upLink, rows) => { - Contents of ${rel} + Contents of ${escapeHtml(rel)} - - - - - - - - - - - - - - - - - - - - - - -

...submitting your project to BrowserFest...

- ` - - const svgContainer = document.createElement('div') - svgContainer.className = 'files-widget__overlay__svg' - svgContainer.innerHTML = uploadSvg - this.innerHTML = svgContainer - } - - _createHTML () { - if (!this.owner || !this.repo) { - this.innerHTML = ` -

You don't have a project currently open. In order to submit to BrowserFest, open an old project or create a new one and then I can submit it for you. To create a new project (rather than simply a sketch) you need to connect me to your GitHub account. If you don't already have a GitHub account you can create one for free.

- ` - return - } - - this.innerHTML = ` - -
- -
- -
- -
- - - -
- ` - - const thumb = this.$('[name="thumbnail"]') - thumb.style.color = 'var(--netizen-hint-color)' - thumb.style.backgroundColor = 'var(--netizen-meta)' - thumb.style.borderRadius = '5px' - thumb.style.margin = '6px' - thumb.addEventListener('click', () => this.fu.input.click()) - - this.$('[name="submit"]') - .addEventListener('click', () => this._preSubmitForkCheck()) - } -} - -window.BrowserFest = BrowserFest diff --git a/www/widgets/browser-fest/styles.css b/www/widgets/browser-fest/styles.css deleted file mode 100644 index e69de29b..00000000 diff --git a/www/widgets/code-review/convo.js b/www/widgets/code-review/convo.js index 8edffe5d..284daaa4 100644 --- a/www/widgets/code-review/convo.js +++ b/www/widgets/code-review/convo.js @@ -91,6 +91,48 @@ window.CONVOS['code-review'] = (self) => { }, // console error convos { + id: 'sandbox-security-error', + content: 'It looks like you\'re trying to use browser storage (like localStorage, sessionStorage, or indexedDB). This sketch was loaded from an external source, so it runs in a sandboxed iframe for security, which blocks browser storage APIs. If you\'d like to use this API start a new sketch or open your own project.', + options: { + 'ok, got it': (e) => e.hide(), + 'what\'s a project?': (e) => e.goTo('sandbox-security-error-project') + } + }, { + id: 'sandbox-security-error-project', + content: 'Unlike a sketch (which is a single HTML file) a project is a folder that can hold unlimited files, including code, images, fonts, and sounds. To get started, click my face to open the Coding Menu and connect your GitHub account. Browser storage APIs like localStorage will work as expected in projects', + options: { + 'ok thanks': (e) => e.hide() + } + }, { + id: 'sensor-sandbox-blocked', + content: 'It looks like this sketch tried to access the camera, microphone, or location. This sketch was loaded from an external source, so it runs in a sandboxed iframe for security, which blocks some browser APIs. If you trust the author of this sketch I can remove those protections?', + options: { + 'trust?': (e) => e.goTo('sensor-sandbox-trust'), + 'yes, I trust the author': (e) => { + NNE.iframe.removeAttribute('sandbox') + NNE.update() + e.goTo('sensor-remove-sandbox') + }, + 'oh, never mind': (e) => { e.hide(); window.convo = null } + } + }, { + id: 'sensor-sandbox-trust', + content: 'If you didn\'t write this code it\'s important to trust the person who did. This is true anytime you run someone else\'s code in your environment, this could mean importing a library someone else wrote into your own sketch, installing a new extention in your browser or even installing an app you downloaded to your computer. If you didn\'t write the code, always make sure it\'s coming from a tusted source!', + options: { + 'Oh, never mind': (e) => e.hide(), + 'I see, yes I trust this!': (e) => { + NNE.iframe.removeAttribute('sandbox') + NNE.update() + e.goTo('sensor-remove-sandbox') + } + } + }, { + id: 'sensor-remove-sandbox', + content: 'Great! I\'ve removed the security sandbox, you should now be able to access the camera, microphone, location and other previously blocked browser APIs.', + options: { + 'ok thanks': (e) => e.hide() + } + }, { id: 'custom-renderer-error', content: `Your browser passed me this error:
${self.error.message}`, options: { diff --git a/www/widgets/code-review/index.js b/www/widgets/code-review/index.js index 81141ba3..32e1031f 100644 --- a/www/widgets/code-review/index.js +++ b/www/widgets/code-review/index.js @@ -13,6 +13,8 @@ class CodeReview extends Widget { this.tempCode = null this.issues = [] // issues netnet catches via linting this.error = {} // last error passed by browser console + this._resourceErrors = [] // failed resource URLs from iframe capture listener + this._runtimeError = null // last JS runtime error; persists across review() calls this._createHTML() this.on('open', () => this._opened()) @@ -21,18 +23,55 @@ class CodeReview extends Widget { Convo.load(this.key, () => { this.convos = window.CONVOS[this.key](this) }) } + /* + NOTE: this has gone through a lot of upates/changes over time, it may need + some refactoring at some point, when we do that, lets use this to test: + + http://localhost:8001/?layout=dock-left#code/eJxlks1SgzAQx+99ip1coKMm9w5w8aQHPdQXSMMCsSTBzaJ2Or67CdSv8QIzu/+PHwnVIbSnZrMBqKzrIZKpxcA8xZ1SBwpH9DczjdIEp5yN0fpeTr4XoEeuxWUC1ukeRZNCco6GgbCrhWllbzsBbXjzY9BtLZjmJGOM/D2slF7bnba+kVK+acu5RcpKLbOVrSPt8Acv0eVXFwhfkRY80VRqla2WaMhO3AAsVAAm+NSbI6FO9WZ26Fm+zEinPY5oOFBZ5HWxXfQdshnKQunJqt7yMB/UHJF8Kiiu4QyGsE0JVo9xB0VM45tAtk9++FgTACQP6EuCugGSzzH4cvt31WrWeZt7pfUe6QnfOQHe7x8fZGRKR2G706L7sRqd0TAbz/+txWEM5ohtooIrwC+a5ZE2etynT00XJiPyHaMrhTvdahbXIJ6CE7/UXQjl9nKr6nKem0otv8wnjiKvJg== + + for security purposes, we've updated the way the render iframe works so that + when loading external/third-party code (shortcode URLs, #code/ hashes, + non-owned GitHub repos) the iframe gets sandboxed. while working on these + updats I ran into a number of side-effects, which then required updates to + this widget and how it handled a set of special error cases. And so, if we + update things in the future we need to first confirm that the sandbox is + still in place, the fetch request in the test sketch should update
+ with the "blocked" message (NOT a GitHub payload) + + we should also see 4 error markers in this sketch: + 1. a CORS error on line 3 for the + 2. a warning about download attributes on line 5 + 3. a mixed-content warning for the