mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Allow render HTML with css/js external links (#19017)
* Allow render HTML with css/js external links * Fix bug because of filename escape chars * Fix lint * Update docs about new configuration item * Fix bug of render HTML in sub directory * Add CSP head for displaying iframe in rendering file * Fix test * Apply suggestions from code review Co-authored-by: delvh <dev.lh@web.de> * Some improvements * some improvement * revert change in SanitizerDisabled of external renderer * Add sandbox for iframe and support allow-scripts and allow-same-origin * refactor * fix * fix lint * fine tune * use single option RENDER_CONTENT_MODE, use sandbox=allow-scripts * fine tune CSP * Apply suggestions from code review Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -33,9 +33,6 @@ func (Renderer) Name() string { | ||||
| 	return MarkupName | ||||
| } | ||||
|  | ||||
| // NeedPostProcess implements markup.Renderer | ||||
| func (Renderer) NeedPostProcess() bool { return false } | ||||
|  | ||||
| // Extensions implements markup.Renderer | ||||
| func (Renderer) Extensions() []string { | ||||
| 	return []string{".sh-session"} | ||||
| @@ -48,11 +45,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SanitizerDisabled disabled sanitize if return true | ||||
| func (Renderer) SanitizerDisabled() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // CanRender implements markup.RendererContentDetector | ||||
| func (Renderer) CanRender(filename string, input io.Reader) bool { | ||||
| 	buf, err := io.ReadAll(input) | ||||
|   | ||||
| @@ -29,9 +29,6 @@ func (Renderer) Name() string { | ||||
| 	return "csv" | ||||
| } | ||||
|  | ||||
| // NeedPostProcess implements markup.Renderer | ||||
| func (Renderer) NeedPostProcess() bool { return false } | ||||
|  | ||||
| // Extensions implements markup.Renderer | ||||
| func (Renderer) Extensions() []string { | ||||
| 	return []string{".csv", ".tsv"} | ||||
| @@ -46,11 +43,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SanitizerDisabled disabled sanitize if return true | ||||
| func (Renderer) SanitizerDisabled() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func writeField(w io.Writer, element, class, field string) error { | ||||
| 	if _, err := io.WriteString(w, "<"); err != nil { | ||||
| 		return err | ||||
|   | ||||
							
								
								
									
										12
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							| @@ -34,6 +34,11 @@ type Renderer struct { | ||||
| 	*setting.MarkupRenderer | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	_ markup.PostProcessRenderer = (*Renderer)(nil) | ||||
| 	_ markup.ExternalRenderer    = (*Renderer)(nil) | ||||
| ) | ||||
|  | ||||
| // Name returns the external tool name | ||||
| func (p *Renderer) Name() string { | ||||
| 	return p.MarkupName | ||||
| @@ -56,7 +61,12 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | ||||
|  | ||||
| // SanitizerDisabled disabled sanitize if return true | ||||
| func (p *Renderer) SanitizerDisabled() bool { | ||||
| 	return p.DisableSanitizer | ||||
| 	return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe | ||||
| } | ||||
|  | ||||
| // DisplayInIFrame represents whether render the content with an iframe | ||||
| func (p *Renderer) DisplayInIFrame() bool { | ||||
| 	return p.RenderContentMode == setting.RenderContentModeIframe | ||||
| } | ||||
|  | ||||
| func envMark(envName string) string { | ||||
|   | ||||
| @@ -29,10 +29,10 @@ func TestRender_Commits(t *testing.T) { | ||||
| 	setting.AppURL = TestAppURL | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Ctx:       git.DefaultContext, | ||||
| 			Filename:  ".md", | ||||
| 			URLPrefix: TestRepoURL, | ||||
| 			Metas:     localMetas, | ||||
| 			Ctx:          git.DefaultContext, | ||||
| 			RelativePath: ".md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 			Metas:        localMetas, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| @@ -80,9 +80,9 @@ func TestRender_CrossReferences(t *testing.T) { | ||||
|  | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			URLPrefix: setting.AppSubURL, | ||||
| 			Metas:     localMetas, | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    setting.AppSubURL, | ||||
| 			Metas:        localMetas, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| @@ -124,8 +124,8 @@ func TestRender_links(t *testing.T) { | ||||
|  | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			URLPrefix: TestRepoURL, | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| @@ -223,8 +223,8 @@ func TestRender_email(t *testing.T) { | ||||
|  | ||||
| 	test := func(input, expected string) { | ||||
| 		res, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			URLPrefix: TestRepoURL, | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) | ||||
| @@ -281,8 +281,8 @@ func TestRender_emoji(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		expected = strings.ReplaceAll(expected, "&", "&") | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			URLPrefix: TestRepoURL, | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
|   | ||||
| @@ -205,12 +205,14 @@ func init() { | ||||
| // Renderer implements markup.Renderer | ||||
| type Renderer struct{} | ||||
|  | ||||
| var _ markup.PostProcessRenderer = (*Renderer)(nil) | ||||
|  | ||||
| // Name implements markup.Renderer | ||||
| func (Renderer) Name() string { | ||||
| 	return MarkupName | ||||
| } | ||||
|  | ||||
| // NeedPostProcess implements markup.Renderer | ||||
| // NeedPostProcess implements markup.PostProcessRenderer | ||||
| func (Renderer) NeedPostProcess() bool { return true } | ||||
|  | ||||
| // Extensions implements markup.Renderer | ||||
| @@ -223,11 +225,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | ||||
| 	return []setting.MarkupSanitizerRule{} | ||||
| } | ||||
|  | ||||
| // SanitizerDisabled disabled sanitize if return true | ||||
| func (Renderer) SanitizerDisabled() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Render implements markup.Renderer | ||||
| func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	return render(ctx, input, output) | ||||
|   | ||||
| @@ -29,12 +29,14 @@ func init() { | ||||
| // Renderer implements markup.Renderer for orgmode | ||||
| type Renderer struct{} | ||||
|  | ||||
| var _ markup.PostProcessRenderer = (*Renderer)(nil) | ||||
|  | ||||
| // Name implements markup.Renderer | ||||
| func (Renderer) Name() string { | ||||
| 	return "orgmode" | ||||
| } | ||||
|  | ||||
| // NeedPostProcess implements markup.Renderer | ||||
| // NeedPostProcess implements markup.PostProcessRenderer | ||||
| func (Renderer) NeedPostProcess() bool { return true } | ||||
|  | ||||
| // Extensions implements markup.Renderer | ||||
| @@ -47,11 +49,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | ||||
| 	return []setting.MarkupSanitizerRule{} | ||||
| } | ||||
|  | ||||
| // SanitizerDisabled disabled sanitize if return true | ||||
| func (Renderer) SanitizerDisabled() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Render renders orgmode rawbytes to HTML | ||||
| func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	htmlWriter := org.NewHTMLWriter() | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/url" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| @@ -43,17 +44,18 @@ type Header struct { | ||||
|  | ||||
| // RenderContext represents a render context | ||||
| type RenderContext struct { | ||||
| 	Ctx             context.Context | ||||
| 	Filename        string | ||||
| 	Type            string | ||||
| 	IsWiki          bool | ||||
| 	URLPrefix       string | ||||
| 	Metas           map[string]string | ||||
| 	DefaultLink     string | ||||
| 	GitRepo         *git.Repository | ||||
| 	ShaExistCache   map[string]bool | ||||
| 	cancelFn        func() | ||||
| 	TableOfContents []Header | ||||
| 	Ctx              context.Context | ||||
| 	RelativePath     string // relative path from tree root of the branch | ||||
| 	Type             string | ||||
| 	IsWiki           bool | ||||
| 	URLPrefix        string | ||||
| 	Metas            map[string]string | ||||
| 	DefaultLink      string | ||||
| 	GitRepo          *git.Repository | ||||
| 	ShaExistCache    map[string]bool | ||||
| 	cancelFn         func() | ||||
| 	TableOfContents  []Header | ||||
| 	InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page | ||||
| } | ||||
|  | ||||
| // Cancel runs any cleanup functions that have been registered for this Ctx | ||||
| @@ -88,12 +90,24 @@ func (ctx *RenderContext) AddCancel(fn func()) { | ||||
| type Renderer interface { | ||||
| 	Name() string // markup format name | ||||
| 	Extensions() []string | ||||
| 	NeedPostProcess() bool | ||||
| 	SanitizerRules() []setting.MarkupSanitizerRule | ||||
| 	SanitizerDisabled() bool | ||||
| 	Render(ctx *RenderContext, input io.Reader, output io.Writer) error | ||||
| } | ||||
|  | ||||
| // PostProcessRenderer defines an interface for renderers who need post process | ||||
| type PostProcessRenderer interface { | ||||
| 	NeedPostProcess() bool | ||||
| } | ||||
|  | ||||
| // PostProcessRenderer defines an interface for external renderers | ||||
| type ExternalRenderer interface { | ||||
| 	// SanitizerDisabled disabled sanitize if return true | ||||
| 	SanitizerDisabled() bool | ||||
|  | ||||
| 	// DisplayInIFrame represents whether render the content with an iframe | ||||
| 	DisplayInIFrame() bool | ||||
| } | ||||
|  | ||||
| // RendererContentDetector detects if the content can be rendered | ||||
| // by specified renderer | ||||
| type RendererContentDetector interface { | ||||
| @@ -142,7 +156,7 @@ func DetectRendererType(filename string, input io.Reader) string { | ||||
| func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	if ctx.Type != "" { | ||||
| 		return renderByType(ctx, input, output) | ||||
| 	} else if ctx.Filename != "" { | ||||
| 	} else if ctx.RelativePath != "" { | ||||
| 		return renderFile(ctx, input, output) | ||||
| 	} | ||||
| 	return errors.New("Render options both filename and type missing") | ||||
| @@ -163,6 +177,27 @@ type nopCloser struct { | ||||
|  | ||||
| func (nopCloser) Close() error { return nil } | ||||
|  | ||||
| func renderIFrame(ctx *RenderContext, output io.Writer) error { | ||||
| 	// set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight) | ||||
| 	// at the moment, only "allow-scripts" is allowed for sandbox mode. | ||||
| 	// "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token | ||||
| 	// TODO: when using dark theme, if the rendered content doesn't have proper style, the default text color is black, which is not easy to read | ||||
| 	_, err := io.WriteString(output, fmt.Sprintf(` | ||||
| <iframe src="%s/%s/%s/render/%s/%s" | ||||
| name="giteaExternalRender" | ||||
| onload="this.height=giteaExternalRender.document.documentElement.scrollHeight" | ||||
| width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden" | ||||
| sandbox="allow-scripts" | ||||
| ></iframe>`, | ||||
| 		setting.AppSubURL, | ||||
| 		url.PathEscape(ctx.Metas["user"]), | ||||
| 		url.PathEscape(ctx.Metas["repo"]), | ||||
| 		ctx.Metas["BranchNameSubURL"], | ||||
| 		url.PathEscape(ctx.RelativePath), | ||||
| 	)) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { | ||||
| 	var wg sync.WaitGroup | ||||
| 	var err error | ||||
| @@ -175,7 +210,12 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr | ||||
| 	var pr2 io.ReadCloser | ||||
| 	var pw2 io.WriteCloser | ||||
|  | ||||
| 	if !renderer.SanitizerDisabled() { | ||||
| 	var sanitizerDisabled bool | ||||
| 	if r, ok := renderer.(ExternalRenderer); ok { | ||||
| 		sanitizerDisabled = r.SanitizerDisabled() | ||||
| 	} | ||||
|  | ||||
| 	if !sanitizerDisabled { | ||||
| 		pr2, pw2 = io.Pipe() | ||||
| 		defer func() { | ||||
| 			_ = pr2.Close() | ||||
| @@ -194,7 +234,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr | ||||
|  | ||||
| 	wg.Add(1) | ||||
| 	go func() { | ||||
| 		if renderer.NeedPostProcess() { | ||||
| 		if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { | ||||
| 			err = PostProcess(ctx, pr, pw2) | ||||
| 		} else { | ||||
| 			_, err = io.Copy(pw2, pr) | ||||
| @@ -239,8 +279,15 @@ func (err ErrUnsupportedRenderExtension) Error() string { | ||||
| } | ||||
|  | ||||
| func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	extension := strings.ToLower(filepath.Ext(ctx.Filename)) | ||||
| 	extension := strings.ToLower(filepath.Ext(ctx.RelativePath)) | ||||
| 	if renderer, ok := extRenderers[extension]; ok { | ||||
| 		if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() { | ||||
| 			if !ctx.InStandalonePage { | ||||
| 				// for an external 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) | ||||
| 	} | ||||
| 	return ErrUnsupportedRenderExtension{extension} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user