mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01: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:
		| @@ -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