Jelajahi Sumber

ipynb: sanitize rendered HTML (#5996)

* ipynb: sanitize rendered HTML

Fixes #5170

* Remove hardcode URL

* Add tests
ᴜɴᴋɴᴡᴏɴ 5 tahun lalu
induk
melakukan
69cb408e6d

+ 2 - 0
CHANGELOG.md

@@ -32,12 +32,14 @@ All notable changes to Gogs are documented in this file.
 - Configuration option `[auth] ENABLE_NOTIFY_MAIL` is deprecated and will end support in 0.13.0, please start using `[user] ENABLE_EMAIL_NOTIFICATION`.
 - Configuration option `[session] GC_INTERVAL_TIME` is deprecated and will end support in 0.13.0, please start using `[session] GC_INTERVAL`.
 - Configuration option `[session] SESSION_LIFE_TIME` is deprecated and will end support in 0.13.0, please start using `[session] MAX_LIFE_TIME`.
+- The name `-` is reserved and cannot be used for users or organizations.
 
 ### Fixed
 
 - [Security] Potential open redirection with i18n.
 - [Security] Potential ability to delete files outside a repository.
 - [Security] Potential ability to set primary email on others' behalf from their verified emails.
+- [Security] Potential XSS attack via `.ipynb`. [#5170](https://github.com/gogs/gogs/issues/5170)
 - [Security] Potential RCE on mirror repositories. [#5767](https://github.com/gogs/gogs/issues/5767)
 - [Security] Potential XSS attack with raw markdown API. [#5907](https://github.com/gogs/gogs/pull/5907)
 - Open/close milestone redirects to a 404 page. [#5677](https://github.com/gogs/gogs/issues/5677)

+ 4 - 0
go.sum

@@ -10,6 +10,7 @@ github.com/G-Node/libgin v0.0.0-20191216094436-47f8aadc0067/go.mod h1:2yLXQnNbwj
 github.com/G-Node/libgin v0.3.0/go.mod h1:VjulCBq7k/kgf4Eabk2f4w9SDNowWhLnK+yZvy5Nppk=
 github.com/G-Node/libgin v0.3.2 h1:pbOIm+paF4VqfrTvSu4sPXmo2j5XSOnfxf137uzAHIs=
 github.com/G-Node/libgin v0.3.2/go.mod h1:/2l42QsLZTneVp2LjPoPOWhVjvfbO26atvcbvWA+axI=
+github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
 github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -152,7 +153,9 @@ github.com/issue9/identicon v1.0.1/go.mod h1:UKNVkUFI68RPz/RlLhsAr1aX6bBSaYEWRHV
 github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
 github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
+github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
 github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -292,6 +295,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/t-tiger/gorm-bulk-insert v1.3.0 h1:9k7BaVEhw/3fsvh6GTOBwJ2RXk3asc5xs5m6hwozq20=
 github.com/t-tiger/gorm-bulk-insert v1.3.0/go.mod h1:ruDlk8xDl+8sX4bA7PQuYly9YEb3pbp1eP2LCyeRrFY=
 github.com/unknwon/cae v1.0.0 h1:i39lOFaBXZxhGjQOy/RNbi8uzettCs6OQxpR0xXohGU=
 github.com/unknwon/cae v1.0.0/go.mod h1:QaSeRctcea9fK6piJpAMCCPKxzJ01+xFcr2k1m3WRPU=

+ 36 - 0
internal/app/api.go

@@ -0,0 +1,36 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package app
+
+import (
+	"net/http"
+
+	"github.com/microcosm-cc/bluemonday"
+	"gopkg.in/macaron.v1"
+
+	"github.com/G-Node/gogs/internal/context"
+)
+
+func ipynbSanitizer() *bluemonday.Policy {
+	p := bluemonday.UGCPolicy()
+	p.AllowAttrs("class", "data-prompt-number").OnElements("div")
+	p.AllowAttrs("class").OnElements("img")
+	p.AllowURLSchemes("data")
+	return p
+}
+
+func SanitizeIpynb() macaron.Handler {
+	p := ipynbSanitizer()
+
+	return func(c *context.Context) {
+		html, err := c.Req.Body().String()
+		if err != nil {
+			c.Error(err, "read body")
+			return
+		}
+
+		c.PlainText(http.StatusOK, p.Sanitize(html))
+	}
+}

+ 95 - 0
internal/app/api_test.go

@@ -0,0 +1,95 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package app
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_ipynbSanitizer(t *testing.T) {
+	p := ipynbSanitizer()
+
+	tests := []struct {
+		name  string
+		input string
+		want  string
+	}{
+		{
+			name: "allow 'class' and 'data-prompt-number' attributes",
+			input: `
+<div class="nb-notebook">
+    <div class="nb-worksheet">
+        <div class="nb-cell nb-markdown-cell">Hello world</div>
+        <div class="nb-cell nb-code-cell">
+            <div class="nb-input" data-prompt-number="4">
+            </div>
+        </div>
+    </div>
+</div>
+`,
+			want: `
+<div class="nb-notebook">
+    <div class="nb-worksheet">
+        <div class="nb-cell nb-markdown-cell">Hello world</div>
+        <div class="nb-cell nb-code-cell">
+            <div class="nb-input" data-prompt-number="4">
+            </div>
+        </div>
+    </div>
+</div>
+`,
+		},
+		{
+			name: "allow base64 encoded images",
+			input: `
+<div class="nb-output" data-prompt-number="4">
+    <img class="nb-image-output" src="data:image/png;base64,iVBORw0KGgoA"/>
+</div>
+`,
+			want: `
+<div class="nb-output" data-prompt-number="4">
+    <img class="nb-image-output" src="data:image/png;base64,iVBORw0KGgoA"/>
+</div>
+`,
+		},
+		{
+			name: "prevent XSS",
+			input: `
+<div class="nb-output" data-prompt-number="10">
+<div class="nb-html-output">
+<style>
+.output {
+align-items: center;
+background: #00ff00;
+}
+</style>
+<script>
+function test() {
+alert("test");
+}
+
+$(document).ready(test);
+</script>
+</div>
+</div>
+`,
+			want: `
+<div class="nb-output" data-prompt-number="10">
+<div class="nb-html-output">
+
+
+</div>
+</div>
+`,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			assert.Equal(t, test.want, p.Sanitize(test.input))
+		})
+	}
+}

+ 29 - 0
internal/app/metrics.go

@@ -0,0 +1,29 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package app
+
+import (
+	"net/http"
+
+	"gopkg.in/macaron.v1"
+
+	"github.com/G-Node/gogs/internal/conf"
+	"github.com/G-Node/gogs/internal/context"
+)
+
+func MetricsFilter() macaron.Handler {
+	return func(c *context.Context) {
+		if !conf.Prometheus.Enabled {
+			c.Status(http.StatusNotFound)
+			return
+		}
+
+		if !conf.Prometheus.EnableBasicAuth {
+			return
+		}
+
+		c.RequireBasicAuth(conf.Prometheus.BasicAuthUsername, conf.Prometheus.BasicAuthPassword)
+	}
+}

File diff ditekan karena terlalu besar
+ 2 - 2
internal/assets/public/public_gen.go


File diff ditekan karena terlalu besar
+ 1 - 1
internal/assets/templates/templates_gen.go


+ 8 - 8
internal/cmd/web.go

@@ -29,6 +29,7 @@ import (
 	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
 
+	"github.com/G-Node/gogs/internal/app"
 	"github.com/G-Node/gogs/internal/assets/public"
 	"github.com/G-Node/gogs/internal/assets/templates"
 	"github.com/G-Node/gogs/internal/conf"
@@ -688,16 +689,15 @@ func runWeb(c *cli.Context) error {
 		apiv1.RegisterRoutes(m)
 	}, ignSignIn)
 
+	// ***************************
+	// ----- Internal routes -----
+	// ***************************
 	m.Group("/-", func() {
-		if conf.Prometheus.Enabled {
-			m.Get("/metrics", func(c *context.Context) {
-				if !conf.Prometheus.EnableBasicAuth {
-					return
-				}
+		m.Get("/metrics", app.MetricsFilter(), promhttp.Handler()) // "/-/metrics"
 
-				c.RequireBasicAuth(conf.Prometheus.BasicAuthUsername, conf.Prometheus.BasicAuthPassword)
-			}, promhttp.Handler())
-		}
+		m.Group("/api", func() {
+			m.Post("/sanitize_ipynb", app.SanitizeIpynb()) // "/-/api/sanitize_ipynb"
+		})
 	})
 
 	// robots.txt

+ 1 - 1
internal/db/user.go

@@ -534,7 +534,7 @@ func NewGhostUser() *User {
 }
 
 var (
-	reservedUsernames    = []string{"explore", "create", "assets", "css", "img", "js", "less", "plugins", "debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new", ".", ".."}
+	reservedUsernames    = []string{"-", "explore", "create", "assets", "css", "img", "js", "less", "plugins", "debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new", ".", ".."}
 	reservedUserPatterns = []string{"*.keys"}
 )
 

File diff ditekan karena terlalu besar
+ 0 - 5
public/plugins/marked-0.3.6/marked.min.js


File diff ditekan karena terlalu besar
+ 5 - 0
public/plugins/marked-0.8.1/marked.min.js


File diff ditekan karena terlalu besar
+ 0 - 0
public/plugins/notebookjs-0.3.0/notebook.min.js


File diff ditekan karena terlalu besar
+ 0 - 0
public/plugins/notebookjs-0.4.2/notebook.min.js


+ 2 - 2
templates/base/head.tmpl

@@ -44,8 +44,8 @@
 
 	<!-- notebook.js for rendering ipython notebooks and marked.js for rendering markdown in notebooks -->
 	{{if .IsIPythonNotebook}}
-		<script src="{{AppSubURL}}/plugins/notebookjs-0.3.0/notebook.min.js"></script>
-		<script src="{{AppSubURL}}/plugins/marked-0.3.6/marked.min.js"></script>
+		<script src="{{AppSubURL}}/plugins/notebookjs-0.4.2/notebook.min.js"></script>
+		<script src="{{AppSubURL}}/plugins/marked-0.8.1/marked.min.js"></script>
 	{{end}}
 
 	{{if .RequireSimpleMDE}}

+ 25 - 18
templates/repo/view_file.tmpl

@@ -103,27 +103,34 @@ var json = editor.get();
 					</script>
 				{{else if .IsIPythonNotebook}}
 					<script>
-						var rendered = null;
 $.getJSON("{{.RawFileLink}}", null, function(notebook_json) {
 	var notebook = nb.parse(notebook_json);
-	rendered = notebook.render();
-	$("#ipython-notebook").append(rendered);
-	$("#ipython-notebook code").each(function(i, block) {
-		$(block).addClass("py").addClass("python");
-		hljs.highlightBlock(block);
-	});
+						var rendered = notebook.render();
+						$.ajax({
+							type: "POST",
+							url: '{{AppSubURL}}/-/api/sanitize_ipynb',
+							data: rendered.outerHTML,
+							processData: false,
+							contentType: false,
+						}).done(function(data) {
+							$("#ipython-notebook").append(data);
+							$("#ipython-notebook code").each(function(i, block) {
+								$(block).addClass("py").addClass("python");
+								hljs.highlightBlock(block);
+							});
 
-	// Overwrite image method to append proper prefix to the source URL
-	var renderer = new marked.Renderer();
-	var context = '{{.RawFileLink}}';
-	context = context.substring(0, context.lastIndexOf("/"));
-	renderer.image = function (href, title, text) {
-		return `<img src="${context}/${href}"`
-	}
-	$("#ipython-notebook .nb-markdown-cell").each(function(i, markdown) {
-		$(markdown).html(marked($(markdown).html(), {renderer: renderer}));
-	});
-});
+							// Overwrite image method to append proper prefix to the source URL
+							var renderer = new marked.Renderer();
+							var context = '{{.RawFileLink}}';
+							context = context.substring(0, context.lastIndexOf("/"));
+							renderer.image = function (href, title, text) {
+								return `<img src="${context}/${href}"`
+							};
+							$("#ipython-notebook .nb-markdown-cell").each(function(i, markdown) {
+								$(markdown).html(marked($(markdown).html(), {renderer: renderer}));
+							});
+						});
+					});
 					</script>
 	{{else if .ReadmeInList}}
 		{{if .FileContent}}{{.FileContent | Str2HTML}}{{end}}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini