Skip to content

Commit b01dce2

Browse files
lunnydelvhwxiaoguang
authored
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 <[email protected]> * 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 <[email protected]> Co-authored-by: delvh <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 7d1770c commit b01dce2

File tree

17 files changed

+248
-93
lines changed

17 files changed

+248
-93
lines changed

custom/conf/app.example.ini

+5-2
Original file line numberDiff line numberDiff line change
@@ -2181,8 +2181,11 @@ PATH =
21812181
;RENDER_COMMAND = "asciidoc --out-file=- -"
21822182
;; Don't pass the file on STDIN, pass the filename as argument instead.
21832183
;IS_INPUT_FILE = false
2184-
; Don't filter html tags and attributes if true
2185-
;DISABLE_SANITIZER = false
2184+
;; How the content will be rendered.
2185+
;; * 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.*] .
2186+
;; * 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.
2187+
;; * 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.
2188+
;RENDER_CONTENT_MODE=sanitized
21862189

21872190
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
21882191
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -1026,13 +1026,16 @@ IS_INPUT_FILE = false
10261026
command. Multiple extensions needs a comma as splitter.
10271027
- RENDER\_COMMAND: External command to render all matching extensions.
10281028
- IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`.
1029-
- DISABLE_SANITIZER: **false** Don't filter html tags and attributes if true. Don't change this to true except you know what that means.
1029+
- RENDER_CONTENT_MODE: **sanitized** How the content will be rendered.
1030+
- 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.*]`.
1031+
- 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.
1032+
- 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.
10301033

10311034
Two special environment variables are passed to the render command:
10321035
- `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links.
10331036
- `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths.
10341037

1035-
If `DISABLE_SANITIZER` is false, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
1038+
If `RENDER_CONTENT_MODE` is `sanitized`, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
10361039

10371040
```ini
10381041
[markup.sanitizer.TeX]

docs/content/doc/advanced/config-cheat-sheet.zh-cn.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -318,14 +318,17 @@ IS_INPUT_FILE = false
318318
- FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。
319319
- RENDER_COMMAND: 工具的命令行命令及参数。
320320
- IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。
321-
- DISABLE_SANITIZER: **false** 如果为 true 则不过滤 HTML 标签和属性。除非你知道这意味着什么,否则不要设置为 true。
321+
- RENDER_CONTENT_MODE: **sanitized** 内容如何被渲染。
322+
- sanitized: 对内容进行净化并渲染到当前页面中,仅有一部分 HTML 标签和属性是被允许的。
323+
- no-sanitizer: 禁用净化器,把内容渲染到当前页面中。此模式是**不安全**的,如果内容中含有恶意代码,可能会导致 XSS 攻击。
324+
- iframe: 把内容渲染在一个独立的页面中并使用 iframe 嵌入到当前页面中。使用的 iframe 工作在沙箱模式并禁用了同源请求,JS 代码被安全的从父页面中隔离出去。
322325

323326
以下两个环境变量将会被传递给渲染命令:
324327

325328
- `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀,可以被用于链接的前缀。
326329
- `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。
327330

328-
如果 `DISABLE_SANITIZER`false,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
331+
如果 `RENDER_CONTENT_MODE``sanitized`,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
329332

330333
```ini
331334
[markup.sanitizer.TeX]

modules/csv/csv.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader)
5454
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
5555
extension := ".csv"
5656
if ctx != nil {
57-
extension = strings.ToLower(filepath.Ext(ctx.Filename))
57+
extension = strings.ToLower(filepath.Ext(ctx.RelativePath))
5858
}
5959

6060
var delimiter rune

modules/csv/csv_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ John Doe [email protected] This,note,had,a,lot,of,commas,to,test,delimiters`,
230230
}
231231

232232
for n, c := range cases {
233-
delimiter := determineDelimiter(&markup.RenderContext{Filename: c.filename}, []byte(decodeSlashes(t, c.csv)))
233+
delimiter := determineDelimiter(&markup.RenderContext{RelativePath: c.filename}, []byte(decodeSlashes(t, c.csv)))
234234
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
235235
}
236236
}

modules/markup/console/console.go

-8
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ func (Renderer) Name() string {
3333
return MarkupName
3434
}
3535

36-
// NeedPostProcess implements markup.Renderer
37-
func (Renderer) NeedPostProcess() bool { return false }
38-
3936
// Extensions implements markup.Renderer
4037
func (Renderer) Extensions() []string {
4138
return []string{".sh-session"}
@@ -48,11 +45,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
4845
}
4946
}
5047

51-
// SanitizerDisabled disabled sanitize if return true
52-
func (Renderer) SanitizerDisabled() bool {
53-
return false
54-
}
55-
5648
// CanRender implements markup.RendererContentDetector
5749
func (Renderer) CanRender(filename string, input io.Reader) bool {
5850
buf, err := io.ReadAll(input)

modules/markup/csv/csv.go

-8
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ func (Renderer) Name() string {
2929
return "csv"
3030
}
3131

32-
// NeedPostProcess implements markup.Renderer
33-
func (Renderer) NeedPostProcess() bool { return false }
34-
3532
// Extensions implements markup.Renderer
3633
func (Renderer) Extensions() []string {
3734
return []string{".csv", ".tsv"}
@@ -46,11 +43,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
4643
}
4744
}
4845

49-
// SanitizerDisabled disabled sanitize if return true
50-
func (Renderer) SanitizerDisabled() bool {
51-
return false
52-
}
53-
5446
func writeField(w io.Writer, element, class, field string) error {
5547
if _, err := io.WriteString(w, "<"); err != nil {
5648
return err

modules/markup/external/external.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type Renderer struct {
3434
*setting.MarkupRenderer
3535
}
3636

37+
var (
38+
_ markup.PostProcessRenderer = (*Renderer)(nil)
39+
_ markup.ExternalRenderer = (*Renderer)(nil)
40+
)
41+
3742
// Name returns the external tool name
3843
func (p *Renderer) Name() string {
3944
return p.MarkupName
@@ -56,7 +61,12 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
5661

5762
// SanitizerDisabled disabled sanitize if return true
5863
func (p *Renderer) SanitizerDisabled() bool {
59-
return p.DisableSanitizer
64+
return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
65+
}
66+
67+
// DisplayInIFrame represents whether render the content with an iframe
68+
func (p *Renderer) DisplayInIFrame() bool {
69+
return p.RenderContentMode == setting.RenderContentModeIframe
6070
}
6171

6272
func envMark(envName string) string {

modules/markup/html_test.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func TestRender_Commits(t *testing.T) {
2929
setting.AppURL = TestAppURL
3030
test := func(input, expected string) {
3131
buffer, err := RenderString(&RenderContext{
32-
Ctx: git.DefaultContext,
33-
Filename: ".md",
34-
URLPrefix: TestRepoURL,
35-
Metas: localMetas,
32+
Ctx: git.DefaultContext,
33+
RelativePath: ".md",
34+
URLPrefix: TestRepoURL,
35+
Metas: localMetas,
3636
}, input)
3737
assert.NoError(t, err)
3838
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -80,9 +80,9 @@ func TestRender_CrossReferences(t *testing.T) {
8080

8181
test := func(input, expected string) {
8282
buffer, err := RenderString(&RenderContext{
83-
Filename: "a.md",
84-
URLPrefix: setting.AppSubURL,
85-
Metas: localMetas,
83+
RelativePath: "a.md",
84+
URLPrefix: setting.AppSubURL,
85+
Metas: localMetas,
8686
}, input)
8787
assert.NoError(t, err)
8888
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -124,8 +124,8 @@ func TestRender_links(t *testing.T) {
124124

125125
test := func(input, expected string) {
126126
buffer, err := RenderString(&RenderContext{
127-
Filename: "a.md",
128-
URLPrefix: TestRepoURL,
127+
RelativePath: "a.md",
128+
URLPrefix: TestRepoURL,
129129
}, input)
130130
assert.NoError(t, err)
131131
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -223,8 +223,8 @@ func TestRender_email(t *testing.T) {
223223

224224
test := func(input, expected string) {
225225
res, err := RenderString(&RenderContext{
226-
Filename: "a.md",
227-
URLPrefix: TestRepoURL,
226+
RelativePath: "a.md",
227+
URLPrefix: TestRepoURL,
228228
}, input)
229229
assert.NoError(t, err)
230230
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
@@ -281,8 +281,8 @@ func TestRender_emoji(t *testing.T) {
281281
test := func(input, expected string) {
282282
expected = strings.ReplaceAll(expected, "&", "&amp;")
283283
buffer, err := RenderString(&RenderContext{
284-
Filename: "a.md",
285-
URLPrefix: TestRepoURL,
284+
RelativePath: "a.md",
285+
URLPrefix: TestRepoURL,
286286
}, input)
287287
assert.NoError(t, err)
288288
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))

modules/markup/markdown/markdown.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,14 @@ func init() {
205205
// Renderer implements markup.Renderer
206206
type Renderer struct{}
207207

208+
var _ markup.PostProcessRenderer = (*Renderer)(nil)
209+
208210
// Name implements markup.Renderer
209211
func (Renderer) Name() string {
210212
return MarkupName
211213
}
212214

213-
// NeedPostProcess implements markup.Renderer
215+
// NeedPostProcess implements markup.PostProcessRenderer
214216
func (Renderer) NeedPostProcess() bool { return true }
215217

216218
// Extensions implements markup.Renderer
@@ -223,11 +225,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
223225
return []setting.MarkupSanitizerRule{}
224226
}
225227

226-
// SanitizerDisabled disabled sanitize if return true
227-
func (Renderer) SanitizerDisabled() bool {
228-
return false
229-
}
230-
231228
// Render implements markup.Renderer
232229
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
233230
return render(ctx, input, output)

modules/markup/orgmode/orgmode.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ func init() {
2929
// Renderer implements markup.Renderer for orgmode
3030
type Renderer struct{}
3131

32+
var _ markup.PostProcessRenderer = (*Renderer)(nil)
33+
3234
// Name implements markup.Renderer
3335
func (Renderer) Name() string {
3436
return "orgmode"
3537
}
3638

37-
// NeedPostProcess implements markup.Renderer
39+
// NeedPostProcess implements markup.PostProcessRenderer
3840
func (Renderer) NeedPostProcess() bool { return true }
3941

4042
// Extensions implements markup.Renderer
@@ -47,11 +49,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
4749
return []setting.MarkupSanitizerRule{}
4850
}
4951

50-
// SanitizerDisabled disabled sanitize if return true
51-
func (Renderer) SanitizerDisabled() bool {
52-
return false
53-
}
54-
5552
// Render renders orgmode rawbytes to HTML
5653
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
5754
htmlWriter := org.NewHTMLWriter()

modules/markup/renderer.go

+64-17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"errors"
1111
"fmt"
1212
"io"
13+
"net/url"
1314
"path/filepath"
1415
"strings"
1516
"sync"
@@ -43,17 +44,18 @@ type Header struct {
4344

4445
// RenderContext represents a render context
4546
type RenderContext struct {
46-
Ctx context.Context
47-
Filename string
48-
Type string
49-
IsWiki bool
50-
URLPrefix string
51-
Metas map[string]string
52-
DefaultLink string
53-
GitRepo *git.Repository
54-
ShaExistCache map[string]bool
55-
cancelFn func()
56-
TableOfContents []Header
47+
Ctx context.Context
48+
RelativePath string // relative path from tree root of the branch
49+
Type string
50+
IsWiki bool
51+
URLPrefix string
52+
Metas map[string]string
53+
DefaultLink string
54+
GitRepo *git.Repository
55+
ShaExistCache map[string]bool
56+
cancelFn func()
57+
TableOfContents []Header
58+
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
5759
}
5860

5961
// Cancel runs any cleanup functions that have been registered for this Ctx
@@ -88,12 +90,24 @@ func (ctx *RenderContext) AddCancel(fn func()) {
8890
type Renderer interface {
8991
Name() string // markup format name
9092
Extensions() []string
91-
NeedPostProcess() bool
9293
SanitizerRules() []setting.MarkupSanitizerRule
93-
SanitizerDisabled() bool
9494
Render(ctx *RenderContext, input io.Reader, output io.Writer) error
9595
}
9696

97+
// PostProcessRenderer defines an interface for renderers who need post process
98+
type PostProcessRenderer interface {
99+
NeedPostProcess() bool
100+
}
101+
102+
// PostProcessRenderer defines an interface for external renderers
103+
type ExternalRenderer interface {
104+
// SanitizerDisabled disabled sanitize if return true
105+
SanitizerDisabled() bool
106+
107+
// DisplayInIFrame represents whether render the content with an iframe
108+
DisplayInIFrame() bool
109+
}
110+
97111
// RendererContentDetector detects if the content can be rendered
98112
// by specified renderer
99113
type RendererContentDetector interface {
@@ -142,7 +156,7 @@ func DetectRendererType(filename string, input io.Reader) string {
142156
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
143157
if ctx.Type != "" {
144158
return renderByType(ctx, input, output)
145-
} else if ctx.Filename != "" {
159+
} else if ctx.RelativePath != "" {
146160
return renderFile(ctx, input, output)
147161
}
148162
return errors.New("Render options both filename and type missing")
@@ -163,6 +177,27 @@ type nopCloser struct {
163177

164178
func (nopCloser) Close() error { return nil }
165179

180+
func renderIFrame(ctx *RenderContext, output io.Writer) error {
181+
// set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
182+
// at the moment, only "allow-scripts" is allowed for sandbox mode.
183+
// "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
184+
// 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
185+
_, err := io.WriteString(output, fmt.Sprintf(`
186+
<iframe src="%s/%s/%s/render/%s/%s"
187+
name="giteaExternalRender"
188+
onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
189+
width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
190+
sandbox="allow-scripts"
191+
></iframe>`,
192+
setting.AppSubURL,
193+
url.PathEscape(ctx.Metas["user"]),
194+
url.PathEscape(ctx.Metas["repo"]),
195+
ctx.Metas["BranchNameSubURL"],
196+
url.PathEscape(ctx.RelativePath),
197+
))
198+
return err
199+
}
200+
166201
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
167202
var wg sync.WaitGroup
168203
var err error
@@ -175,7 +210,12 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
175210
var pr2 io.ReadCloser
176211
var pw2 io.WriteCloser
177212

178-
if !renderer.SanitizerDisabled() {
213+
var sanitizerDisabled bool
214+
if r, ok := renderer.(ExternalRenderer); ok {
215+
sanitizerDisabled = r.SanitizerDisabled()
216+
}
217+
218+
if !sanitizerDisabled {
179219
pr2, pw2 = io.Pipe()
180220
defer func() {
181221
_ = pr2.Close()
@@ -194,7 +234,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
194234

195235
wg.Add(1)
196236
go func() {
197-
if renderer.NeedPostProcess() {
237+
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
198238
err = PostProcess(ctx, pr, pw2)
199239
} else {
200240
_, err = io.Copy(pw2, pr)
@@ -239,8 +279,15 @@ func (err ErrUnsupportedRenderExtension) Error() string {
239279
}
240280

241281
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
242-
extension := strings.ToLower(filepath.Ext(ctx.Filename))
282+
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
243283
if renderer, ok := extRenderers[extension]; ok {
284+
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
285+
if !ctx.InStandalonePage {
286+
// for an external render, it could only output its content in a standalone page
287+
// otherwise, a <iframe> should be outputted to embed the external rendered page
288+
return renderIFrame(ctx, output)
289+
}
290+
}
244291
return render(ctx, renderer, input, output)
245292
}
246293
return ErrUnsupportedRenderExtension{extension}

0 commit comments

Comments
 (0)