mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00: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. | ;; * 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. | ;; * 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 | ;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/markup" | ||||||
| 	"code.gitea.io/gitea/modules/process" | 	"code.gitea.io/gitea/modules/process" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  |  | ||||||
|  | 	"github.com/kballard/go-shellquote" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // RegisterRenderers registers all supported third part renderers according settings | // 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_SRC"), baseLinkSrc, | ||||||
| 		envMark("GITEA_PREFIX_RAW"), baseLinkRaw, | 		envMark("GITEA_PREFIX_RAW"), baseLinkRaw, | ||||||
| 	).Replace(p.Command) | 	).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:] | 	args := commands[1:] | ||||||
|  |  | ||||||
| 	if p.IsInputFile { | 	if p.IsInputFile { | ||||||
|   | |||||||
| @@ -120,31 +120,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext { | |||||||
| 	return ctx | 	return ctx | ||||||
| } | } | ||||||
|  |  | ||||||
| // Render renders markup file to HTML with all specific handling stuff. | // FindRendererByContext finds renderer by RenderContext | ||||||
| func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | // 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 != "" { | 	if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { | ||||||
| 		ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) | 		ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) | ||||||
| 		if ctx.RenderOptions.MarkupType == "" { | 		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] | 	renderer := renderers[ctx.RenderOptions.MarkupType] | ||||||
| 	if renderer == nil { | 	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 != "" { | 	return renderer, nil | ||||||
| 		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 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 | // 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 | 	ctx.usedByRender = true | ||||||
| 	if ctx.RenderHelper != nil { | 	if ctx.RenderHelper != nil { | ||||||
| 		defer ctx.RenderHelper.CleanUp() | 		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) { | 	eg.Go(func() (err error) { | ||||||
| 		if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { | 		if RendererNeedPostProcess(renderer) { | ||||||
| 			err = PostProcessDefault(ctx, pr1, pw2) | 			err = PostProcessDefault(ctx, pr1, pw2) | ||||||
| 		} else { | 		} else { | ||||||
| 			_, err = io.Copy(pw2, pr1) | 			_, err = io.Copy(pw2, pr1) | ||||||
|   | |||||||
| @@ -259,7 +259,9 @@ func newMarkupRenderer(name string, sec ConfigSection) { | |||||||
| 		FileExtensions:    exts, | 		FileExtensions:    exts, | ||||||
| 		Command:           command, | 		Command:           command, | ||||||
| 		IsInputFile:       sec.Key("IS_INPUT_FILE").MustBool(false), | 		IsInputFile:       sec.Key("IS_INPUT_FILE").MustBool(false), | ||||||
| 		NeedPostProcess:   sec.Key("NEED_POSTPROCESS").MustBool(true), |  | ||||||
| 		RenderContentMode: renderContentMode, | 		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) { | 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() | 	markupRd, markupWr := io.Pipe() | ||||||
| 	defer markupWr.Close() | 	defer markupWr.Close() | ||||||
|  |  | ||||||
| 	done := make(chan struct{}) | 	done := make(chan struct{}) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		sb := &strings.Builder{} | 		sb := &strings.Builder{} | ||||||
| 		// We allow NBSP here this is rendered | 		if markup.RendererNeedPostProcess(renderer) { | ||||||
| 		escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) | 			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()) | 		output = template.HTML(sb.String()) | ||||||
| 		close(done) | 		close(done) | ||||||
| 	}() | 	}() | ||||||
| 	err = markup.Render(renderCtx, input, markupWr) |  | ||||||
|  | 	err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr) | ||||||
| 	_ = markupWr.CloseWithError(err) | 	_ = markupWr.CloseWithError(err) | ||||||
| 	<-done | 	<-done | ||||||
| 	return escaped, output, err | 	return escaped, output, err | ||||||
|   | |||||||
| @@ -4,18 +4,23 @@ | |||||||
| package integration | package integration | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"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" | ||||||
| 	"code.gitea.io/gitea/modules/markup/external" | 	"code.gitea.io/gitea/modules/markup/external" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/test" | ||||||
| 	"code.gitea.io/gitea/tests" | 	"code.gitea.io/gitea/tests" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestExternalMarkupRenderer(t *testing.T) { | func TestExternalMarkupRenderer(t *testing.T) { | ||||||
| @@ -25,36 +30,52 @@ func TestExternalMarkupRenderer(t *testing.T) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	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) | ||||||
|  |  | ||||||
|  | 			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)) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("RenderContentDirectly", func(t *testing.T) { | ||||||
| 		req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | 		req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | ||||||
| 		resp := MakeRequest(t, req, http.StatusOK) | 		resp := MakeRequest(t, req, http.StatusOK) | ||||||
| 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||||
|  |  | ||||||
| 	bs, err := io.ReadAll(resp.Body) | 		doc := NewHTMLParser(t, resp.Body) | ||||||
| 	assert.NoError(t, err) |  | ||||||
|  |  | ||||||
| 	doc := NewHTMLParser(t, bytes.NewBuffer(bs)) |  | ||||||
| 		div := doc.Find("div.file-view") | 		div := doc.Find("div.file-view") | ||||||
| 		data, err := div.Html() | 		data, err := div.Html() | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data)) | 		assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data)) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	r := markup.GetRendererByFileName("a.html").(*external.Renderer) | 	r := markup.GetRendererByFileName("any-file.html").(*external.Renderer) | ||||||
| 	r.RenderContentMode = setting.RenderContentModeIframe | 	defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)() | ||||||
|  |  | ||||||
| 	req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") | 	t.Run("RenderContentInIFrame", func(t *testing.T) { | ||||||
| 	resp = MakeRequest(t, req, http.StatusOK) | 		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")) | 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||||
| 	bs, err = io.ReadAll(resp.Body) | 		doc := NewHTMLParser(t, resp.Body) | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	doc = NewHTMLParser(t, bytes.NewBuffer(bs)) |  | ||||||
| 		iframe := doc.Find("iframe") | 		iframe := doc.Find("iframe") | ||||||
| 		assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", "")) | 		assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", "")) | ||||||
|  |  | ||||||
| 		req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") | 		req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") | ||||||
| 		resp = MakeRequest(t, req, http.StatusOK) | 		resp = MakeRequest(t, req, http.StatusOK) | ||||||
| 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | 		assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) | ||||||
| 	bs, err = io.ReadAll(resp.Body) | 		bs, err := io.ReadAll(resp.Body) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy")) | 		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)) | 		assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs)) | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -114,9 +114,16 @@ ENABLED = true | |||||||
| [markup.html] | [markup.html] | ||||||
| ENABLED = true | ENABLED = true | ||||||
| FILE_EXTENSIONS = .html | FILE_EXTENSIONS = .html | ||||||
| RENDER_COMMAND = `go run build/test-echo.go` | RENDER_COMMAND = go run build/test-echo.go | ||||||
| IS_INPUT_FILE = false | ;RENDER_COMMAND = cat | ||||||
| RENDER_CONTENT_MODE=sanitized | ;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] | [actions] | ||||||
| ENABLED = true | ENABLED = true | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user