Skip to content

Commit 733fbb6

Browse files
authored
Merge branch 'release/v1.25' into lunny/frontport_35339
2 parents cf4fd76 + b2f2f85 commit 733fbb6

File tree

19 files changed

+395
-125
lines changed

19 files changed

+395
-125
lines changed

custom/conf/app.example.ini

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2540,7 +2540,19 @@ LEVEL = Info
25402540
;; * 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.*] .
25412541
;; * 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.
25422542
;; * 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.
2543-
;RENDER_CONTENT_MODE=sanitized
2543+
;RENDER_CONTENT_MODE = sanitized
2544+
;; The sandbox applied to the iframe and Content-Security-Policy header when RENDER_CONTENT_MODE is `iframe`.
2545+
;; It defaults to a safe set of "allow-*" restrictions (space separated).
2546+
;; You can also set it by your requirements or use "disabled" to disable the sandbox completely.
2547+
;; When set it, make sure there is no security risk:
2548+
;; * PDF-only content: generally safe to use "disabled", and it needs to be "disabled" because PDF only renders with no sandbox.
2549+
;; * HTML content with JS: if the "RENDER_COMMAND" can guarantee there is no XSS, then it is safe, otherwise, you need to fine tune the "allow-*" restrictions.
2550+
;RENDER_CONTENT_SANDBOX =
2551+
;; Whether post-process the rendered HTML content, including:
2552+
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
2553+
;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc.
2554+
;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false.
2555+
;NEED_POST_PROCESS = false
25442556

25452557
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
25462558
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

modules/httplib/serve.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt
126126
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
127127
// should generally be safe as scripts inside PDF can not escape the PDF document
128128
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
129+
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
129130
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
130131
}
131132

modules/markup/external/external.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"code.gitea.io/gitea/modules/markup"
1616
"code.gitea.io/gitea/modules/process"
1717
"code.gitea.io/gitea/modules/setting"
18+
19+
"github.com/kballard/go-shellquote"
1820
)
1921

2022
// RegisterRenderers registers all supported third part renderers according settings
@@ -56,14 +58,11 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
5658
return p.MarkupSanitizerRules
5759
}
5860

59-
// SanitizerDisabled disabled sanitize if return true
60-
func (p *Renderer) SanitizerDisabled() bool {
61-
return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
62-
}
63-
64-
// DisplayInIFrame represents whether render the content with an iframe
65-
func (p *Renderer) DisplayInIFrame() bool {
66-
return p.RenderContentMode == setting.RenderContentModeIframe
61+
func (p *Renderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
62+
ret.SanitizerDisabled = p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
63+
ret.DisplayInIframe = p.RenderContentMode == setting.RenderContentModeIframe
64+
ret.ContentSandbox = p.RenderContentSandbox
65+
return ret
6766
}
6867

6968
func envMark(envName string) string {
@@ -81,7 +80,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
8180
envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
8281
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
8382
).Replace(p.Command)
84-
commands := strings.Fields(command)
83+
commands, err := shellquote.Split(command)
84+
if err != nil || len(commands) == 0 {
85+
return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err)
86+
}
8587
args := commands[1:]
8688

8789
if p.IsInputFile {

modules/markup/internal/finalprocessor.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ package internal
55

66
import (
77
"bytes"
8+
"html/template"
89
"io"
910
)
1011

1112
type finalProcessor struct {
1213
renderInternal *RenderInternal
14+
extraHeadHTML template.HTML
1315

1416
output io.Writer
1517
buf bytes.Buffer
@@ -25,6 +27,32 @@ func (p *finalProcessor) Close() error {
2527
// because "postProcess" already does so. In the future we could optimize the code to process data on the fly.
2628
buf := p.buf.Bytes()
2729
buf = bytes.ReplaceAll(buf, []byte(` data-attr-class="`+p.renderInternal.secureIDPrefix), []byte(` class="`))
28-
_, err := p.output.Write(buf)
30+
31+
tmp := bytes.TrimSpace(buf)
32+
isLikelyHTML := len(tmp) != 0 && tmp[0] == '<' && tmp[len(tmp)-1] == '>' && bytes.Index(tmp, []byte(`</`)) > 0
33+
if !isLikelyHTML {
34+
// not HTML, write back directly
35+
_, err := p.output.Write(buf)
36+
return err
37+
}
38+
39+
// add our extra head HTML into output
40+
headBytes := []byte("<head>")
41+
posHead := bytes.Index(buf, headBytes)
42+
var part1, part2 []byte
43+
if posHead >= 0 {
44+
part1, part2 = buf[:posHead+len(headBytes)], buf[posHead+len(headBytes):]
45+
} else {
46+
part1, part2 = nil, buf
47+
}
48+
if len(part1) > 0 {
49+
if _, err := p.output.Write(part1); err != nil {
50+
return err
51+
}
52+
}
53+
if _, err := io.WriteString(p.output, string(p.extraHeadHTML)); err != nil {
54+
return err
55+
}
56+
_, err := p.output.Write(part2)
2957
return err
3058
}

modules/markup/internal/internal_test.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/stretchr/testify/assert"
1313
)
1414

15-
func TestRenderInternal(t *testing.T) {
15+
func TestRenderInternalAttrs(t *testing.T) {
1616
cases := []struct {
1717
input, protected, recovered string
1818
}{
@@ -30,7 +30,7 @@ func TestRenderInternal(t *testing.T) {
3030
for _, c := range cases {
3131
var r RenderInternal
3232
out := &bytes.Buffer{}
33-
in := r.init("sec", out)
33+
in := r.init("sec", out, "")
3434
protected := r.ProtectSafeAttrs(template.HTML(c.input))
3535
assert.EqualValues(t, c.protected, protected)
3636
_, _ = io.WriteString(in, string(protected))
@@ -41,7 +41,7 @@ func TestRenderInternal(t *testing.T) {
4141
var r1, r2 RenderInternal
4242
protected := r1.ProtectSafeAttrs(`<div class="test"></div>`)
4343
assert.EqualValues(t, `<div class="test"></div>`, protected, "non-initialized RenderInternal should not protect any attributes")
44-
_ = r1.init("sec", nil)
44+
_ = r1.init("sec", nil, "")
4545
protected = r1.ProtectSafeAttrs(`<div class="test"></div>`)
4646
assert.EqualValues(t, `<div data-attr-class="sec:test"></div>`, protected)
4747
assert.Equal(t, "data-attr-class", r1.SafeAttr("class"))
@@ -54,8 +54,37 @@ func TestRenderInternal(t *testing.T) {
5454
assert.Empty(t, recovered)
5555

5656
out2 := &bytes.Buffer{}
57-
in2 := r2.init("sec-other", out2)
57+
in2 := r2.init("sec-other", out2, "")
5858
_, _ = io.WriteString(in2, string(protected))
5959
_ = in2.Close()
6060
assert.Equal(t, `<div data-attr-class="sec:test"></div>`, out2.String(), "different secureID should not recover the value")
6161
}
62+
63+
func TestRenderInternalExtraHead(t *testing.T) {
64+
t.Run("HeadExists", func(t *testing.T) {
65+
out := &bytes.Buffer{}
66+
var r RenderInternal
67+
in := r.init("sec", out, `<MY-TAG>`)
68+
_, _ = io.WriteString(in, `<head>any</head>`)
69+
_ = in.Close()
70+
assert.Equal(t, `<head><MY-TAG>any</head>`, out.String())
71+
})
72+
73+
t.Run("HeadNotExists", func(t *testing.T) {
74+
out := &bytes.Buffer{}
75+
var r RenderInternal
76+
in := r.init("sec", out, `<MY-TAG>`)
77+
_, _ = io.WriteString(in, `<div></div>`)
78+
_ = in.Close()
79+
assert.Equal(t, `<MY-TAG><div></div>`, out.String())
80+
})
81+
82+
t.Run("NotHTML", func(t *testing.T) {
83+
out := &bytes.Buffer{}
84+
var r RenderInternal
85+
in := r.init("sec", out, `<MY-TAG>`)
86+
_, _ = io.WriteString(in, `<any>`)
87+
_ = in.Close()
88+
assert.Equal(t, `<any>`, out.String())
89+
})
90+
}

modules/markup/internal/renderinternal.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ type RenderInternal struct {
2929
secureIDPrefix string
3030
}
3131

32-
func (r *RenderInternal) Init(output io.Writer) io.WriteCloser {
32+
func (r *RenderInternal) Init(output io.Writer, extraHeadHTML template.HTML) io.WriteCloser {
3333
buf := make([]byte, 12)
3434
_, err := rand.Read(buf)
3535
if err != nil {
3636
panic("unable to generate secure id")
3737
}
38-
return r.init(base64.URLEncoding.EncodeToString(buf), output)
38+
return r.init(base64.URLEncoding.EncodeToString(buf), output, extraHeadHTML)
3939
}
4040

41-
func (r *RenderInternal) init(secID string, output io.Writer) io.WriteCloser {
41+
func (r *RenderInternal) init(secID string, output io.Writer, extraHeadHTML template.HTML) io.WriteCloser {
4242
r.secureID = secID
4343
r.secureIDPrefix = r.secureID + ":"
44-
return &finalProcessor{renderInternal: r, output: output}
44+
return &finalProcessor{renderInternal: r, output: output, extraHeadHTML: extraHeadHTML}
4545
}
4646

4747
func (r *RenderInternal) RecoverProtectedValue(v string) (string, bool) {

modules/markup/render.go

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ package markup
66
import (
77
"context"
88
"fmt"
9+
"html/template"
910
"io"
1011
"net/url"
1112
"strconv"
1213
"strings"
1314
"time"
1415

16+
"code.gitea.io/gitea/modules/htmlutil"
1517
"code.gitea.io/gitea/modules/markup/internal"
1618
"code.gitea.io/gitea/modules/setting"
1719
"code.gitea.io/gitea/modules/util"
@@ -120,31 +122,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
120122
return ctx
121123
}
122124

123-
// Render renders markup file to HTML with all specific handling stuff.
124-
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
125+
// FindRendererByContext finds renderer by RenderContext
126+
// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
127+
func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
125128
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
126129
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
127130
if ctx.RenderOptions.MarkupType == "" {
128-
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
131+
return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
129132
}
130133
}
131134

132135
renderer := renderers[ctx.RenderOptions.MarkupType]
133136
if renderer == nil {
134-
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
137+
return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
135138
}
136139

137-
if ctx.RenderOptions.RelativePath != "" {
138-
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
139-
if !ctx.RenderOptions.InStandalonePage {
140-
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
141-
// otherwise, a <iframe> should be outputted to embed the external rendered page
142-
return renderIFrame(ctx, output)
143-
}
144-
}
140+
return renderer, nil
141+
}
142+
143+
func RendererNeedPostProcess(renderer Renderer) bool {
144+
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
145+
return true
145146
}
147+
return false
148+
}
146149

147-
return render(ctx, renderer, input, output)
150+
// Render renders markup file to HTML with all specific handling stuff.
151+
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
152+
renderer, err := FindRendererByContext(ctx)
153+
if err != nil {
154+
return err
155+
}
156+
return RenderWithRenderer(ctx, renderer, input, output)
148157
}
149158

150159
// RenderString renders Markup string to HTML with all specific handling stuff and return string
@@ -156,24 +165,20 @@ func RenderString(ctx *RenderContext, content string) (string, error) {
156165
return buf.String(), nil
157166
}
158167

159-
func renderIFrame(ctx *RenderContext, output io.Writer) error {
160-
// set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
161-
// at the moment, only "allow-scripts" is allowed for sandbox mode.
162-
// "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
163-
// 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
164-
_, err := io.WriteString(output, fmt.Sprintf(`
165-
<iframe src="%s/%s/%s/render/%s/%s"
166-
name="giteaExternalRender"
167-
onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
168-
width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
169-
sandbox="allow-scripts"
170-
></iframe>`,
171-
setting.AppSubURL,
168+
func renderIFrame(ctx *RenderContext, sandbox string, output io.Writer) error {
169+
src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL,
172170
url.PathEscape(ctx.RenderOptions.Metas["user"]),
173171
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
174-
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
175-
url.PathEscape(ctx.RenderOptions.RelativePath),
176-
))
172+
util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]),
173+
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
174+
)
175+
176+
var sandboxAttrValue template.HTML
177+
if sandbox != "" {
178+
sandboxAttrValue = htmlutil.HTMLFormat(`sandbox="%s"`, sandbox)
179+
}
180+
iframe := htmlutil.HTMLFormat(`<iframe data-src="%s" class="external-render-iframe" %s></iframe>`, src, sandboxAttrValue)
181+
_, err := io.WriteString(output, string(iframe))
177182
return err
178183
}
179184

@@ -185,13 +190,34 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
185190
}
186191
}
187192

188-
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
193+
func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) {
194+
if externalRender, ok := renderer.(ExternalRenderer); ok {
195+
return externalRender.GetExternalRendererOptions(), true
196+
}
197+
return ret, false
198+
}
199+
200+
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
201+
var extraHeadHTML template.HTML
202+
if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe {
203+
if !ctx.RenderOptions.InStandalonePage {
204+
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
205+
// otherwise, a <iframe> should be outputted to embed the external rendered page
206+
return renderIFrame(ctx, extOpts.ContentSandbox, output)
207+
}
208+
// else: this is a standalone page, fallthrough to the real rendering, and add extra JS/CSS
209+
extraStyleHref := setting.AppSubURL + "/assets/css/external-render-iframe.css"
210+
extraScriptSrc := setting.AppSubURL + "/assets/js/external-render-iframe.js"
211+
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
212+
extraHeadHTML = htmlutil.HTMLFormat(`<script src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraStyleHref)
213+
}
214+
189215
ctx.usedByRender = true
190216
if ctx.RenderHelper != nil {
191217
defer ctx.RenderHelper.CleanUp()
192218
}
193219

194-
finalProcessor := ctx.RenderInternal.Init(output)
220+
finalProcessor := ctx.RenderInternal.Init(output, extraHeadHTML)
195221
defer finalProcessor.Close()
196222

197223
// input -> (pw1=pr1) -> renderer -> (pw2=pr2) -> SanitizeReader -> finalProcessor -> output
@@ -202,7 +228,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
202228
eg, _ := errgroup.WithContext(ctx)
203229
var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
204230

205-
if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
231+
if r, ok := renderer.(ExternalRenderer); !ok || !r.GetExternalRendererOptions().SanitizerDisabled {
206232
var pr2 io.ReadCloser
207233
var close2 func()
208234
pr2, pw2, close2 = pipes()
@@ -214,7 +240,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
214240
}
215241

216242
eg.Go(func() (err error) {
217-
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
243+
if RendererNeedPostProcess(renderer) {
218244
err = PostProcessDefault(ctx, pr1, pw2)
219245
} else {
220246
_, err = io.Copy(pw2, pr1)

modules/markup/renderer.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ type PostProcessRenderer interface {
2525
NeedPostProcess() bool
2626
}
2727

28+
type ExternalRendererOptions struct {
29+
SanitizerDisabled bool
30+
DisplayInIframe bool
31+
ContentSandbox string
32+
}
33+
2834
// ExternalRenderer defines an interface for external renderers
2935
type ExternalRenderer interface {
30-
// SanitizerDisabled disabled sanitize if return true
31-
SanitizerDisabled() bool
32-
33-
// DisplayInIFrame represents whether render the content with an iframe
34-
DisplayInIFrame() bool
36+
GetExternalRendererOptions() ExternalRendererOptions
3537
}
3638

3739
// RendererContentDetector detects if the content can be rendered

0 commit comments

Comments
 (0)