mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	Fix external render (#35727)
Fix #35725 --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -2541,6 +2541,12 @@ LEVEL = Info | ||||
| ;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code. | ||||
| ;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page. | ||||
| ;RENDER_CONTENT_MODE=sanitized | ||||
| ;; | ||||
| ;; Whether post-process the rendered HTML content, including: | ||||
| ;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters, | ||||
| ;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc. | ||||
| ;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false. | ||||
| ;NEED_POST_PROCESS=false | ||||
|  | ||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||
|   | ||||
							
								
								
									
										7
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							| @@ -15,6 +15,8 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/process" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
|  | ||||
| 	"github.com/kballard/go-shellquote" | ||||
| ) | ||||
|  | ||||
| // RegisterRenderers registers all supported third part renderers according settings | ||||
| @@ -81,7 +83,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. | ||||
| 		envMark("GITEA_PREFIX_SRC"), baseLinkSrc, | ||||
| 		envMark("GITEA_PREFIX_RAW"), baseLinkRaw, | ||||
| 	).Replace(p.Command) | ||||
| 	commands := strings.Fields(command) | ||||
| 	commands, err := shellquote.Split(command) | ||||
| 	if err != nil || len(commands) == 0 { | ||||
| 		return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err) | ||||
| 	} | ||||
| 	args := commands[1:] | ||||
|  | ||||
| 	if p.IsInputFile { | ||||
|   | ||||
| @@ -120,31 +120,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext { | ||||
| 	return ctx | ||||
| } | ||||
|  | ||||
| // Render renders markup file to HTML with all specific handling stuff. | ||||
| func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| // FindRendererByContext finds renderer by RenderContext | ||||
| // TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc | ||||
| func FindRendererByContext(ctx *RenderContext) (Renderer, error) { | ||||
| 	if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { | ||||
| 		ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) | ||||
| 		if ctx.RenderOptions.MarkupType == "" { | ||||
| 			return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) | ||||
| 			return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	renderer := renderers[ctx.RenderOptions.MarkupType] | ||||
| 	if renderer == nil { | ||||
| 		return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) | ||||
| 		return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) | ||||
| 	} | ||||
|  | ||||
| 	if ctx.RenderOptions.RelativePath != "" { | ||||
| 		if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() { | ||||
| 			if !ctx.RenderOptions.InStandalonePage { | ||||
| 				// for an external "DisplayInIFrame" render, it could only output its content in a standalone page | ||||
| 				// otherwise, a <iframe> should be outputted to embed the external rendered page | ||||
| 				return renderIFrame(ctx, output) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return renderer, nil | ||||
| } | ||||
|  | ||||
| 	return render(ctx, renderer, input, output) | ||||
| func RendererNeedPostProcess(renderer Renderer) bool { | ||||
| 	if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Render renders markup file to HTML with all specific handling stuff. | ||||
| func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	renderer, err := FindRendererByContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return RenderWithRenderer(ctx, renderer, input, output) | ||||
| } | ||||
|  | ||||
| // RenderString renders Markup string to HTML with all specific handling stuff and return string | ||||
| @@ -185,7 +192,16 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { | ||||
| func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { | ||||
| 	if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() { | ||||
| 		if !ctx.RenderOptions.InStandalonePage { | ||||
| 			// for an external "DisplayInIFrame" render, it could only output its content in a standalone page | ||||
| 			// otherwise, a <iframe> should be outputted to embed the external rendered page | ||||
| 			return renderIFrame(ctx, output) | ||||
| 		} | ||||
| 		// else: this is a standalone page, fallthrough to the real rendering | ||||
| 	} | ||||
|  | ||||
| 	ctx.usedByRender = true | ||||
| 	if ctx.RenderHelper != nil { | ||||
| 		defer ctx.RenderHelper.CleanUp() | ||||
| @@ -214,7 +230,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr | ||||
| 	} | ||||
|  | ||||
| 	eg.Go(func() (err error) { | ||||
| 		if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { | ||||
| 		if RendererNeedPostProcess(renderer) { | ||||
| 			err = PostProcessDefault(ctx, pr1, pw2) | ||||
| 		} else { | ||||
| 			_, err = io.Copy(pw2, pr1) | ||||
|   | ||||
| @@ -259,7 +259,9 @@ func newMarkupRenderer(name string, sec ConfigSection) { | ||||
| 		FileExtensions:    exts, | ||||
| 		Command:           command, | ||||
| 		IsInputFile:       sec.Key("IS_INPUT_FILE").MustBool(false), | ||||
| 		NeedPostProcess:   sec.Key("NEED_POSTPROCESS").MustBool(true), | ||||
| 		RenderContentMode: renderContentMode, | ||||
|  | ||||
| 		// if no sanitizer is needed, no post process is needed | ||||
| 		NeedPostProcess: sec.Key("NEED_POST_PROCESS").MustBool(renderContentMode == RenderContentModeSanitized), | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -151,17 +151,28 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { | ||||
| } | ||||
|  | ||||
| func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) { | ||||
| 	renderer, err := markup.FindRendererByContext(renderCtx) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
|  | ||||
| 	markupRd, markupWr := io.Pipe() | ||||
| 	defer markupWr.Close() | ||||
|  | ||||
| 	done := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		sb := &strings.Builder{} | ||||
| 		// We allow NBSP here this is rendered | ||||
| 		escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) | ||||
| 		if markup.RendererNeedPostProcess(renderer) { | ||||
| 			escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered | ||||
| 		} else { | ||||
| 			escaped = &charset.EscapeStatus{} | ||||
| 			_, _ = io.Copy(sb, markupRd) | ||||
| 		} | ||||
| 		output = template.HTML(sb.String()) | ||||
| 		close(done) | ||||
| 	}() | ||||
| 	err = markup.Render(renderCtx, input, markupWr) | ||||
|  | ||||
| 	err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr) | ||||
| 	_ = markupWr.CloseWithError(err) | ||||
| 	<-done | ||||
| 	return escaped, output, err | ||||
|   | ||||
| @@ -4,18 +4,23 @@ | ||||
| package integration | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/external" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestExternalMarkupRenderer(t *testing.T) { | ||||
| @@ -25,36 +30,52 @@ func TestExternalMarkupRenderer(t *testing.T) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||
| 	resp := MakeRequest(t, req, http.StatusOK) | ||||
| 	assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 	onGiteaRun(t, func(t *testing.T, _ *url.URL) { | ||||
| 		t.Run("RenderNoSanitizer", func(t *testing.T) { | ||||
| 			user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
| 			repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) | ||||
| 			_, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`) | ||||
| 			require.NoError(t, err) | ||||
|  | ||||
| 	bs, err := io.ReadAll(resp.Body) | ||||
| 	assert.NoError(t, err) | ||||
| 			req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer") | ||||
| 			resp := MakeRequest(t, req, http.StatusOK) | ||||
| 			doc := NewHTMLParser(t, resp.Body) | ||||
| 			div := doc.Find("div.file-view") | ||||
| 			data, err := div.Html() | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.Equal(t, `<script>window.alert("hi")</script>`, strings.TrimSpace(data)) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	doc := NewHTMLParser(t, bytes.NewBuffer(bs)) | ||||
| 	div := doc.Find("div.file-view") | ||||
| 	data, err := div.Html() | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data)) | ||||
| 	t.Run("RenderContentDirectly", func(t *testing.T) { | ||||
| 		req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||
| 		resp := MakeRequest(t, req, http.StatusOK) | ||||
| 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
|  | ||||
| 	r := markup.GetRendererByFileName("a.html").(*external.Renderer) | ||||
| 	r.RenderContentMode = setting.RenderContentModeIframe | ||||
| 		doc := NewHTMLParser(t, resp.Body) | ||||
| 		div := doc.Find("div.file-view") | ||||
| 		data, err := div.Html() | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data)) | ||||
| 	}) | ||||
|  | ||||
| 	req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 	bs, err = io.ReadAll(resp.Body) | ||||
| 	assert.NoError(t, err) | ||||
| 	doc = NewHTMLParser(t, bytes.NewBuffer(bs)) | ||||
| 	iframe := doc.Find("iframe") | ||||
| 	assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", "")) | ||||
| 	r := markup.GetRendererByFileName("any-file.html").(*external.Renderer) | ||||
| 	defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)() | ||||
|  | ||||
| 	req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 	bs, err = io.ReadAll(resp.Body) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy")) | ||||
| 	assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs)) | ||||
| 	t.Run("RenderContentInIFrame", func(t *testing.T) { | ||||
| 		req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||
| 		resp := MakeRequest(t, req, http.StatusOK) | ||||
| 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 		doc := NewHTMLParser(t, resp.Body) | ||||
| 		iframe := doc.Find("iframe") | ||||
| 		assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", "")) | ||||
|  | ||||
| 		req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") | ||||
| 		resp = MakeRequest(t, req, http.StatusOK) | ||||
| 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||
| 		bs, err := io.ReadAll(resp.Body) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy")) | ||||
| 		assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs)) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -114,9 +114,16 @@ ENABLED = true | ||||
| [markup.html] | ||||
| ENABLED = true | ||||
| FILE_EXTENSIONS = .html | ||||
| RENDER_COMMAND = `go run build/test-echo.go` | ||||
| IS_INPUT_FILE = false | ||||
| RENDER_CONTENT_MODE=sanitized | ||||
| RENDER_COMMAND = go run build/test-echo.go | ||||
| ;RENDER_COMMAND = cat | ||||
| ;IS_INPUT_FILE = true | ||||
| RENDER_CONTENT_MODE = sanitized | ||||
|  | ||||
| [markup.no-sanitizer] | ||||
| ENABLED = true | ||||
| FILE_EXTENSIONS = .no-sanitizer | ||||
| RENDER_COMMAND = echo '<script>window.alert("hi")</script>' | ||||
| RENDER_CONTENT_MODE = no-sanitizer | ||||
|  | ||||
| [actions] | ||||
| ENABLED = true | ||||
|   | ||||
		Reference in New Issue
	
	Block a user