mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +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:
		| @@ -2181,8 +2181,11 @@ PATH = | |||||||
| ;RENDER_COMMAND = "asciidoc --out-file=- -" | ;RENDER_COMMAND = "asciidoc --out-file=- -" | ||||||
| ;; Don't pass the file on STDIN, pass the filename as argument instead. | ;; Don't pass the file on STDIN, pass the filename as argument instead. | ||||||
| ;IS_INPUT_FILE = false | ;IS_INPUT_FILE = false | ||||||
| ; Don't filter html tags and attributes if true | ;; How the content will be rendered. | ||||||
| ;DISABLE_SANITIZER = false | ;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] . | ||||||
|  | ;; * 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 | ||||||
|  |  | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|   | |||||||
| @@ -1026,13 +1026,16 @@ IS_INPUT_FILE = false | |||||||
|    command. Multiple extensions needs a comma as splitter. |    command. Multiple extensions needs a comma as splitter. | ||||||
| - RENDER\_COMMAND: External command to render all matching extensions. | - RENDER\_COMMAND: External command to render all matching extensions. | ||||||
| - IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`. | - IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`. | ||||||
| - DISABLE_SANITIZER: **false** Don't filter html tags and attributes if true. Don't change this to true except you know what that means. | - RENDER_CONTENT_MODE: **sanitized** How the content will be rendered. | ||||||
|  |   - sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in `[markup.sanitizer.*]`. | ||||||
|  |   - 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. | ||||||
|  |  | ||||||
| Two special environment variables are passed to the render command: | Two special environment variables are passed to the render command: | ||||||
| - `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links. | - `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links. | ||||||
| - `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths. | - `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths. | ||||||
|  |  | ||||||
| If `DISABLE_SANITIZER` is false, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc. | If `RENDER_CONTENT_MODE` is `sanitized`, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc. | ||||||
|  |  | ||||||
| ```ini | ```ini | ||||||
| [markup.sanitizer.TeX] | [markup.sanitizer.TeX] | ||||||
|   | |||||||
| @@ -318,14 +318,17 @@ IS_INPUT_FILE = false | |||||||
| - FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。 | - FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。 | ||||||
| - RENDER_COMMAND: 工具的命令行命令及参数。 | - RENDER_COMMAND: 工具的命令行命令及参数。 | ||||||
| - IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。 | - IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。 | ||||||
| - DISABLE_SANITIZER: **false** 如果为 true 则不过滤 HTML 标签和属性。除非你知道这意味着什么,否则不要设置为 true。 | - RENDER_CONTENT_MODE: **sanitized** 内容如何被渲染。 | ||||||
|  |   - sanitized: 对内容进行净化并渲染到当前页面中,仅有一部分 HTML 标签和属性是被允许的。 | ||||||
|  |   - no-sanitizer: 禁用净化器,把内容渲染到当前页面中。此模式是**不安全**的,如果内容中含有恶意代码,可能会导致 XSS 攻击。 | ||||||
|  |   - iframe: 把内容渲染在一个独立的页面中并使用 iframe 嵌入到当前页面中。使用的 iframe 工作在沙箱模式并禁用了同源请求,JS 代码被安全的从父页面中隔离出去。 | ||||||
|  |  | ||||||
| 以下两个环境变量将会被传递给渲染命令: | 以下两个环境变量将会被传递给渲染命令: | ||||||
|  |  | ||||||
| - `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀,可以被用于链接的前缀。 | - `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀,可以被用于链接的前缀。 | ||||||
| - `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。 | - `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。 | ||||||
|  |  | ||||||
| 如果 `DISABLE_SANITIZER` 为 false,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。 | 如果 `RENDER_CONTENT_MODE` 为 `sanitized`,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。 | ||||||
|  |  | ||||||
| ```ini | ```ini | ||||||
| [markup.sanitizer.TeX] | [markup.sanitizer.TeX] | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader) | |||||||
| func determineDelimiter(ctx *markup.RenderContext, data []byte) rune { | func determineDelimiter(ctx *markup.RenderContext, data []byte) rune { | ||||||
| 	extension := ".csv" | 	extension := ".csv" | ||||||
| 	if ctx != nil { | 	if ctx != nil { | ||||||
| 		extension = strings.ToLower(filepath.Ext(ctx.Filename)) | 		extension = strings.ToLower(filepath.Ext(ctx.RelativePath)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var delimiter rune | 	var delimiter rune | ||||||
|   | |||||||
| @@ -230,7 +230,7 @@ John Doe	john@doe.com	This,note,had,a,lot,of,commas,to,test,delimiters`, | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for n, c := range cases { | 	for n, c := range cases { | ||||||
| 		delimiter := determineDelimiter(&markup.RenderContext{Filename: c.filename}, []byte(decodeSlashes(t, c.csv))) | 		delimiter := determineDelimiter(&markup.RenderContext{RelativePath: c.filename}, []byte(decodeSlashes(t, c.csv))) | ||||||
| 		assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter) | 		assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,9 +33,6 @@ func (Renderer) Name() string { | |||||||
| 	return MarkupName | 	return MarkupName | ||||||
| } | } | ||||||
|  |  | ||||||
| // NeedPostProcess implements markup.Renderer |  | ||||||
| func (Renderer) NeedPostProcess() bool { return false } |  | ||||||
|  |  | ||||||
| // Extensions implements markup.Renderer | // Extensions implements markup.Renderer | ||||||
| func (Renderer) Extensions() []string { | func (Renderer) Extensions() []string { | ||||||
| 	return []string{".sh-session"} | 	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 | // CanRender implements markup.RendererContentDetector | ||||||
| func (Renderer) CanRender(filename string, input io.Reader) bool { | func (Renderer) CanRender(filename string, input io.Reader) bool { | ||||||
| 	buf, err := io.ReadAll(input) | 	buf, err := io.ReadAll(input) | ||||||
|   | |||||||
| @@ -29,9 +29,6 @@ func (Renderer) Name() string { | |||||||
| 	return "csv" | 	return "csv" | ||||||
| } | } | ||||||
|  |  | ||||||
| // NeedPostProcess implements markup.Renderer |  | ||||||
| func (Renderer) NeedPostProcess() bool { return false } |  | ||||||
|  |  | ||||||
| // Extensions implements markup.Renderer | // Extensions implements markup.Renderer | ||||||
| func (Renderer) Extensions() []string { | func (Renderer) Extensions() []string { | ||||||
| 	return []string{".csv", ".tsv"} | 	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 { | func writeField(w io.Writer, element, class, field string) error { | ||||||
| 	if _, err := io.WriteString(w, "<"); err != nil { | 	if _, err := io.WriteString(w, "<"); err != nil { | ||||||
| 		return err | 		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 | 	*setting.MarkupRenderer | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	_ markup.PostProcessRenderer = (*Renderer)(nil) | ||||||
|  | 	_ markup.ExternalRenderer    = (*Renderer)(nil) | ||||||
|  | ) | ||||||
|  |  | ||||||
| // Name returns the external tool name | // Name returns the external tool name | ||||||
| func (p *Renderer) Name() string { | func (p *Renderer) Name() string { | ||||||
| 	return p.MarkupName | 	return p.MarkupName | ||||||
| @@ -56,7 +61,12 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | |||||||
|  |  | ||||||
| // SanitizerDisabled disabled sanitize if return true | // SanitizerDisabled disabled sanitize if return true | ||||||
| func (p *Renderer) SanitizerDisabled() bool { | 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 { | func envMark(envName string) string { | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ func TestRender_Commits(t *testing.T) { | |||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer, err := RenderString(&RenderContext{ | 		buffer, err := RenderString(&RenderContext{ | ||||||
| 			Ctx:          git.DefaultContext, | 			Ctx:          git.DefaultContext, | ||||||
| 			Filename:  ".md", | 			RelativePath: ".md", | ||||||
| 			URLPrefix:    TestRepoURL, | 			URLPrefix:    TestRepoURL, | ||||||
| 			Metas:        localMetas, | 			Metas:        localMetas, | ||||||
| 		}, input) | 		}, input) | ||||||
| @@ -80,7 +80,7 @@ func TestRender_CrossReferences(t *testing.T) { | |||||||
|  |  | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer, err := RenderString(&RenderContext{ | 		buffer, err := RenderString(&RenderContext{ | ||||||
| 			Filename:  "a.md", | 			RelativePath: "a.md", | ||||||
| 			URLPrefix:    setting.AppSubURL, | 			URLPrefix:    setting.AppSubURL, | ||||||
| 			Metas:        localMetas, | 			Metas:        localMetas, | ||||||
| 		}, input) | 		}, input) | ||||||
| @@ -124,7 +124,7 @@ func TestRender_links(t *testing.T) { | |||||||
|  |  | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer, err := RenderString(&RenderContext{ | 		buffer, err := RenderString(&RenderContext{ | ||||||
| 			Filename:  "a.md", | 			RelativePath: "a.md", | ||||||
| 			URLPrefix:    TestRepoURL, | 			URLPrefix:    TestRepoURL, | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| @@ -223,7 +223,7 @@ func TestRender_email(t *testing.T) { | |||||||
|  |  | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		res, err := RenderString(&RenderContext{ | 		res, err := RenderString(&RenderContext{ | ||||||
| 			Filename:  "a.md", | 			RelativePath: "a.md", | ||||||
| 			URLPrefix:    TestRepoURL, | 			URLPrefix:    TestRepoURL, | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| @@ -281,7 +281,7 @@ func TestRender_emoji(t *testing.T) { | |||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		expected = strings.ReplaceAll(expected, "&", "&") | 		expected = strings.ReplaceAll(expected, "&", "&") | ||||||
| 		buffer, err := RenderString(&RenderContext{ | 		buffer, err := RenderString(&RenderContext{ | ||||||
| 			Filename:  "a.md", | 			RelativePath: "a.md", | ||||||
| 			URLPrefix:    TestRepoURL, | 			URLPrefix:    TestRepoURL, | ||||||
| 		}, input) | 		}, input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
|   | |||||||
| @@ -205,12 +205,14 @@ func init() { | |||||||
| // Renderer implements markup.Renderer | // Renderer implements markup.Renderer | ||||||
| type Renderer struct{} | type Renderer struct{} | ||||||
|  |  | ||||||
|  | var _ markup.PostProcessRenderer = (*Renderer)(nil) | ||||||
|  |  | ||||||
| // Name implements markup.Renderer | // Name implements markup.Renderer | ||||||
| func (Renderer) Name() string { | func (Renderer) Name() string { | ||||||
| 	return MarkupName | 	return MarkupName | ||||||
| } | } | ||||||
|  |  | ||||||
| // NeedPostProcess implements markup.Renderer | // NeedPostProcess implements markup.PostProcessRenderer | ||||||
| func (Renderer) NeedPostProcess() bool { return true } | func (Renderer) NeedPostProcess() bool { return true } | ||||||
|  |  | ||||||
| // Extensions implements markup.Renderer | // Extensions implements markup.Renderer | ||||||
| @@ -223,11 +225,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | |||||||
| 	return []setting.MarkupSanitizerRule{} | 	return []setting.MarkupSanitizerRule{} | ||||||
| } | } | ||||||
|  |  | ||||||
| // SanitizerDisabled disabled sanitize if return true |  | ||||||
| func (Renderer) SanitizerDisabled() bool { |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Render implements markup.Renderer | // Render implements markup.Renderer | ||||||
| func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	return render(ctx, input, output) | 	return render(ctx, input, output) | ||||||
|   | |||||||
| @@ -29,12 +29,14 @@ func init() { | |||||||
| // Renderer implements markup.Renderer for orgmode | // Renderer implements markup.Renderer for orgmode | ||||||
| type Renderer struct{} | type Renderer struct{} | ||||||
|  |  | ||||||
|  | var _ markup.PostProcessRenderer = (*Renderer)(nil) | ||||||
|  |  | ||||||
| // Name implements markup.Renderer | // Name implements markup.Renderer | ||||||
| func (Renderer) Name() string { | func (Renderer) Name() string { | ||||||
| 	return "orgmode" | 	return "orgmode" | ||||||
| } | } | ||||||
|  |  | ||||||
| // NeedPostProcess implements markup.Renderer | // NeedPostProcess implements markup.PostProcessRenderer | ||||||
| func (Renderer) NeedPostProcess() bool { return true } | func (Renderer) NeedPostProcess() bool { return true } | ||||||
|  |  | ||||||
| // Extensions implements markup.Renderer | // Extensions implements markup.Renderer | ||||||
| @@ -47,11 +49,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { | |||||||
| 	return []setting.MarkupSanitizerRule{} | 	return []setting.MarkupSanitizerRule{} | ||||||
| } | } | ||||||
|  |  | ||||||
| // SanitizerDisabled disabled sanitize if return true |  | ||||||
| func (Renderer) SanitizerDisabled() bool { |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Render renders orgmode rawbytes to HTML | // Render renders orgmode rawbytes to HTML | ||||||
| func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	htmlWriter := org.NewHTMLWriter() | 	htmlWriter := org.NewHTMLWriter() | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"net/url" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| @@ -44,7 +45,7 @@ type Header struct { | |||||||
| // RenderContext represents a render context | // RenderContext represents a render context | ||||||
| type RenderContext struct { | type RenderContext struct { | ||||||
| 	Ctx              context.Context | 	Ctx              context.Context | ||||||
| 	Filename        string | 	RelativePath     string // relative path from tree root of the branch | ||||||
| 	Type             string | 	Type             string | ||||||
| 	IsWiki           bool | 	IsWiki           bool | ||||||
| 	URLPrefix        string | 	URLPrefix        string | ||||||
| @@ -54,6 +55,7 @@ type RenderContext struct { | |||||||
| 	ShaExistCache    map[string]bool | 	ShaExistCache    map[string]bool | ||||||
| 	cancelFn         func() | 	cancelFn         func() | ||||||
| 	TableOfContents  []Header | 	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 | // 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 { | type Renderer interface { | ||||||
| 	Name() string // markup format name | 	Name() string // markup format name | ||||||
| 	Extensions() []string | 	Extensions() []string | ||||||
| 	NeedPostProcess() bool |  | ||||||
| 	SanitizerRules() []setting.MarkupSanitizerRule | 	SanitizerRules() []setting.MarkupSanitizerRule | ||||||
| 	SanitizerDisabled() bool |  | ||||||
| 	Render(ctx *RenderContext, input io.Reader, output io.Writer) error | 	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 | // RendererContentDetector detects if the content can be rendered | ||||||
| // by specified renderer | // by specified renderer | ||||||
| type RendererContentDetector interface { | 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 { | func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	if ctx.Type != "" { | 	if ctx.Type != "" { | ||||||
| 		return renderByType(ctx, input, output) | 		return renderByType(ctx, input, output) | ||||||
| 	} else if ctx.Filename != "" { | 	} else if ctx.RelativePath != "" { | ||||||
| 		return renderFile(ctx, input, output) | 		return renderFile(ctx, input, output) | ||||||
| 	} | 	} | ||||||
| 	return errors.New("Render options both filename and type missing") | 	return errors.New("Render options both filename and type missing") | ||||||
| @@ -163,6 +177,27 @@ type nopCloser struct { | |||||||
|  |  | ||||||
| func (nopCloser) Close() error { return nil } | 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 { | func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { | ||||||
| 	var wg sync.WaitGroup | 	var wg sync.WaitGroup | ||||||
| 	var err error | 	var err error | ||||||
| @@ -175,7 +210,12 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr | |||||||
| 	var pr2 io.ReadCloser | 	var pr2 io.ReadCloser | ||||||
| 	var pw2 io.WriteCloser | 	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() | 		pr2, pw2 = io.Pipe() | ||||||
| 		defer func() { | 		defer func() { | ||||||
| 			_ = pr2.Close() | 			_ = pr2.Close() | ||||||
| @@ -194,7 +234,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr | |||||||
|  |  | ||||||
| 	wg.Add(1) | 	wg.Add(1) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		if renderer.NeedPostProcess() { | 		if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { | ||||||
| 			err = PostProcess(ctx, pr, pw2) | 			err = PostProcess(ctx, pr, pw2) | ||||||
| 		} else { | 		} else { | ||||||
| 			_, err = io.Copy(pw2, pr) | 			_, 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 { | 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 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 render(ctx, renderer, input, output) | ||||||
| 	} | 	} | ||||||
| 	return ErrUnsupportedRenderExtension{extension} | 	return ErrUnsupportedRenderExtension{extension} | ||||||
|   | |||||||
| @@ -20,6 +20,12 @@ var ( | |||||||
| 	MermaidMaxSourceCharacters int | 	MermaidMaxSourceCharacters int | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	RenderContentModeSanitized   = "sanitized" | ||||||
|  | 	RenderContentModeNoSanitizer = "no-sanitizer" | ||||||
|  | 	RenderContentModeIframe      = "iframe" | ||||||
|  | ) | ||||||
|  |  | ||||||
| // MarkupRenderer defines the external parser configured in ini | // MarkupRenderer defines the external parser configured in ini | ||||||
| type MarkupRenderer struct { | type MarkupRenderer struct { | ||||||
| 	Enabled              bool | 	Enabled              bool | ||||||
| @@ -29,7 +35,7 @@ type MarkupRenderer struct { | |||||||
| 	IsInputFile          bool | 	IsInputFile          bool | ||||||
| 	NeedPostProcess      bool | 	NeedPostProcess      bool | ||||||
| 	MarkupSanitizerRules []MarkupSanitizerRule | 	MarkupSanitizerRules []MarkupSanitizerRule | ||||||
| 	DisableSanitizer     bool | 	RenderContentMode    string | ||||||
| } | } | ||||||
|  |  | ||||||
| // MarkupSanitizerRule defines the policy for whitelisting attributes on | // MarkupSanitizerRule defines the policy for whitelisting attributes on | ||||||
| @@ -144,6 +150,21 @@ func newMarkupRenderer(name string, sec *ini.Section) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if sec.HasKey("DISABLE_SANITIZER") { | ||||||
|  | 		log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized) | ||||||
|  | 	if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) { | ||||||
|  | 		renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it | ||||||
|  | 	} | ||||||
|  | 	if renderContentMode != RenderContentModeSanitized && | ||||||
|  | 		renderContentMode != RenderContentModeNoSanitizer && | ||||||
|  | 		renderContentMode != RenderContentModeIframe { | ||||||
|  | 		log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized) | ||||||
|  | 		renderContentMode = RenderContentModeSanitized | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{ | 	ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{ | ||||||
| 		Enabled:           sec.Key("ENABLED").MustBool(false), | 		Enabled:           sec.Key("ENABLED").MustBool(false), | ||||||
| 		MarkupName:        name, | 		MarkupName:        name, | ||||||
| @@ -151,6 +172,6 @@ func newMarkupRenderer(name string, sec *ini.Section) { | |||||||
| 		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), | 		NeedPostProcess:   sec.Key("NEED_POSTPROCESS").MustBool(true), | ||||||
| 		DisableSanitizer: sec.Key("DISABLE_SANITIZER").MustBool(false), | 		RenderContentMode: renderContentMode, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -139,7 +139,7 @@ func setCsvCompareContext(ctx *context.Context) { | |||||||
| 			return csvReader, reader, err | 			return csvReader, reader, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, Filename: diffFile.OldName}, baseCommit) | 		baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.OldName}, baseCommit) | ||||||
| 		if baseBlobCloser != nil { | 		if baseBlobCloser != nil { | ||||||
| 			defer baseBlobCloser.Close() | 			defer baseBlobCloser.Close() | ||||||
| 		} | 		} | ||||||
| @@ -151,7 +151,7 @@ func setCsvCompareContext(ctx *context.Context) { | |||||||
| 			return CsvDiffResult{nil, "unable to load file from base commit"} | 			return CsvDiffResult{nil, "unable to load file from base commit"} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, Filename: diffFile.Name}, headCommit) | 		headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.Name}, headCommit) | ||||||
| 		if headBlobCloser != nil { | 		if headBlobCloser != nil { | ||||||
| 			defer headBlobCloser.Close() | 			defer headBlobCloser.Close() | ||||||
| 		} | 		} | ||||||
|   | |||||||
							
								
								
									
										79
									
								
								routers/web/repo/render.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								routers/web/repo/render.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | // Copyright 2022 The Gitea 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 repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"path" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/charset" | ||||||
|  | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 	"code.gitea.io/gitea/modules/typesniffer" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // RenderFile renders a file by repos path | ||||||
|  | func RenderFile(ctx *context.Context) { | ||||||
|  | 	blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if git.IsErrNotExist(err) { | ||||||
|  | 			ctx.NotFound("GetBlobByPath", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("GetBlobByPath", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dataRc, err := blob.DataAsync() | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("DataAsync", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer dataRc.Close() | ||||||
|  |  | ||||||
|  | 	buf := make([]byte, 1024) | ||||||
|  | 	n, _ := util.ReadAtMost(dataRc, buf) | ||||||
|  | 	buf = buf[:n] | ||||||
|  |  | ||||||
|  | 	st := typesniffer.DetectContentType(buf) | ||||||
|  | 	isTextFile := st.IsText() | ||||||
|  |  | ||||||
|  | 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc)) | ||||||
|  |  | ||||||
|  | 	if markupType := markup.Type(blob.Name()); markupType == "" { | ||||||
|  | 		if isTextFile { | ||||||
|  | 			_, err = io.Copy(ctx.Resp, rd) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("Copy", err) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.Error(http.StatusInternalServerError, "Unsupported file type render") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() | ||||||
|  | 	if ctx.Repo.TreePath != "" { | ||||||
|  | 		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts") | ||||||
|  | 	err = markup.Render(&markup.RenderContext{ | ||||||
|  | 		Ctx:              ctx, | ||||||
|  | 		RelativePath:     ctx.Repo.TreePath, | ||||||
|  | 		URLPrefix:        path.Dir(treeLink), | ||||||
|  | 		Metas:            ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
|  | 		GitRepo:          ctx.Repo.GitRepo, | ||||||
|  | 		InStandalonePage: true, | ||||||
|  | 	}, rd, ctx.Resp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("Render", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -357,7 +357,7 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin | |||||||
| 		var result strings.Builder | 		var result strings.Builder | ||||||
| 		err := markup.Render(&markup.RenderContext{ | 		err := markup.Render(&markup.RenderContext{ | ||||||
| 			Ctx:          ctx, | 			Ctx:          ctx, | ||||||
| 			Filename:  readmeFile.name, | 			RelativePath: ctx.Repo.TreePath, | ||||||
| 			URLPrefix:    readmeTreelink, | 			URLPrefix:    readmeTreelink, | ||||||
| 			Metas:        ctx.Repo.Repository.ComposeDocumentMetas(), | 			Metas:        ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
| 			GitRepo:      ctx.Repo.GitRepo, | 			GitRepo:      ctx.Repo.GitRepo, | ||||||
| @@ -528,18 +528,22 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | |||||||
| 			if !detected { | 			if !detected { | ||||||
| 				markupType = "" | 				markupType = "" | ||||||
| 			} | 			} | ||||||
|  | 			metas := ctx.Repo.Repository.ComposeDocumentMetas() | ||||||
|  | 			metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() | ||||||
| 			err := markup.Render(&markup.RenderContext{ | 			err := markup.Render(&markup.RenderContext{ | ||||||
| 				Ctx:          ctx, | 				Ctx:          ctx, | ||||||
| 				Type:         markupType, | 				Type:         markupType, | ||||||
| 				Filename:  blob.Name(), | 				RelativePath: ctx.Repo.TreePath, | ||||||
| 				URLPrefix:    path.Dir(treeLink), | 				URLPrefix:    path.Dir(treeLink), | ||||||
| 				Metas:     ctx.Repo.Repository.ComposeDocumentMetas(), | 				Metas:        metas, | ||||||
| 				GitRepo:      ctx.Repo.GitRepo, | 				GitRepo:      ctx.Repo.GitRepo, | ||||||
| 			}, rd, &result) | 			}, rd, &result) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				ctx.ServerError("Render", err) | 				ctx.ServerError("Render", err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  | 			// to prevent iframe load third-party url | ||||||
|  | 			ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") | ||||||
| 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String()) | 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String()) | ||||||
| 		} else if readmeExist && !shouldRenderSource { | 		} else if readmeExist && !shouldRenderSource { | ||||||
| 			buf := &bytes.Buffer{} | 			buf := &bytes.Buffer{} | ||||||
| @@ -628,7 +632,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | |||||||
| 			var result strings.Builder | 			var result strings.Builder | ||||||
| 			err := markup.Render(&markup.RenderContext{ | 			err := markup.Render(&markup.RenderContext{ | ||||||
| 				Ctx:          ctx, | 				Ctx:          ctx, | ||||||
| 				Filename:  blob.Name(), | 				RelativePath: ctx.Repo.TreePath, | ||||||
| 				URLPrefix:    path.Dir(treeLink), | 				URLPrefix:    path.Dir(treeLink), | ||||||
| 				Metas:        ctx.Repo.Repository.ComposeDocumentMetas(), | 				Metas:        ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
| 				GitRepo:      ctx.Repo.GitRepo, | 				GitRepo:      ctx.Repo.GitRepo, | ||||||
|   | |||||||
| @@ -1161,6 +1161,13 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload) | 			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload) | ||||||
| 		}, repo.MustBeNotEmpty, reqRepoCodeReader) | 		}, repo.MustBeNotEmpty, reqRepoCodeReader) | ||||||
|  |  | ||||||
|  | 		m.Group("/render", func() { | ||||||
|  | 			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RenderFile) | ||||||
|  | 			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RenderFile) | ||||||
|  | 			m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RenderFile) | ||||||
|  | 			m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.RenderFile) | ||||||
|  | 		}, repo.MustBeNotEmpty, reqRepoCodeReader) | ||||||
|  |  | ||||||
| 		m.Group("/commits", func() { | 		m.Group("/commits", func() { | ||||||
| 			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits) | 			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits) | ||||||
| 			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits) | 			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user