mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-24 09:04:23 +02:00
Fix external render (#35727)
Fix #35725 --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
08b9776970
commit
195fc715ff
@ -2541,6 +2541,12 @@ LEVEL = Info
|
||||
;; * 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
|
||||
;;
|
||||
;; Whether post-process the rendered HTML content, including:
|
||||
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
|
||||
;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc.
|
||||
;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false.
|
||||
;NEED_POST_PROCESS=false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
7
modules/markup/external/external.go
vendored
7
modules/markup/external/external.go
vendored
@ -15,6 +15,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
// RegisterRenderers registers all supported third part renderers according settings
|
||||
@ -81,7 +83,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||
envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
|
||||
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
|
||||
).Replace(p.Command)
|
||||
commands := strings.Fields(command)
|
||||
commands, err := shellquote.Split(command)
|
||||
if err != nil || len(commands) == 0 {
|
||||
return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err)
|
||||
}
|
||||
args := commands[1:]
|
||||
|
||||
if p.IsInputFile {
|
||||
|
||||
@ -120,31 +120,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
// FindRendererByContext finds renderer by RenderContext
|
||||
// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
|
||||
func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
|
||||
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
|
||||
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
|
||||
if ctx.RenderOptions.MarkupType == "" {
|
||||
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
|
||||
return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
renderer := renderers[ctx.RenderOptions.MarkupType]
|
||||
if renderer == nil {
|
||||
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
|
||||
return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
|
||||
}
|
||||
|
||||
if ctx.RenderOptions.RelativePath != "" {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
|
||||
if !ctx.RenderOptions.InStandalonePage {
|
||||
// for an external "DisplayInIFrame" 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 renderer, nil
|
||||
}
|
||||
|
||||
return render(ctx, renderer, input, output)
|
||||
func RendererNeedPostProcess(renderer Renderer) bool {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
renderer, err := FindRendererByContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RenderWithRenderer(ctx, renderer, input, output)
|
||||
}
|
||||
|
||||
// RenderString renders Markup string to HTML with all specific handling stuff and return string
|
||||
@ -185,7 +192,16 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
|
||||
if !ctx.RenderOptions.InStandalonePage {
|
||||
// for an external "DisplayInIFrame" 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)
|
||||
}
|
||||
// else: this is a standalone page, fallthrough to the real rendering
|
||||
}
|
||||
|
||||
ctx.usedByRender = true
|
||||
if ctx.RenderHelper != nil {
|
||||
defer ctx.RenderHelper.CleanUp()
|
||||
@ -214,7 +230,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||
}
|
||||
|
||||
eg.Go(func() (err error) {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
if RendererNeedPostProcess(renderer) {
|
||||
err = PostProcessDefault(ctx, pr1, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr1)
|
||||
|
||||
@ -259,7 +259,9 @@ func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
FileExtensions: exts,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
|
||||
RenderContentMode: renderContentMode,
|
||||
|
||||
// if no sanitizer is needed, no post process is needed
|
||||
NeedPostProcess: sec.Key("NEED_POST_PROCESS").MustBool(renderContentMode == RenderContentModeSanitized),
|
||||
})
|
||||
}
|
||||
|
||||
@ -151,17 +151,28 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
||||
}
|
||||
|
||||
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
|
||||
renderer, err := markup.FindRendererByContext(renderCtx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
markupRd, markupWr := io.Pipe()
|
||||
defer markupWr.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
sb := &strings.Builder{}
|
||||
// We allow NBSP here this is rendered
|
||||
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
|
||||
if markup.RendererNeedPostProcess(renderer) {
|
||||
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered
|
||||
} else {
|
||||
escaped = &charset.EscapeStatus{}
|
||||
_, _ = io.Copy(sb, markupRd)
|
||||
}
|
||||
output = template.HTML(sb.String())
|
||||
close(done)
|
||||
}()
|
||||
err = markup.Render(renderCtx, input, markupWr)
|
||||
|
||||
err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr)
|
||||
_ = markupWr.CloseWithError(err)
|
||||
<-done
|
||||
return escaped, output, err
|
||||
|
||||
@ -4,18 +4,23 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/external"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExternalMarkupRenderer(t *testing.T) {
|
||||
@ -25,36 +30,52 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
t.Run("RenderNoSanitizer", func(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`)
|
||||
require.NoError(t, err)
|
||||
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
div := doc.Find("div.file-view")
|
||||
data, err := div.Html()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<script>window.alert("hi")</script>`, strings.TrimSpace(data))
|
||||
})
|
||||
})
|
||||
|
||||
doc := NewHTMLParser(t, bytes.NewBuffer(bs))
|
||||
div := doc.Find("div.file-view")
|
||||
data, err := div.Html()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
||||
t.Run("RenderContentDirectly", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
|
||||
r := markup.GetRendererByFileName("a.html").(*external.Renderer)
|
||||
r.RenderContentMode = setting.RenderContentModeIframe
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
div := doc.Find("div.file-view")
|
||||
data, err := div.Html()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
||||
})
|
||||
|
||||
req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
doc = NewHTMLParser(t, bytes.NewBuffer(bs))
|
||||
iframe := doc.Find("iframe")
|
||||
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
|
||||
r := markup.GetRendererByFileName("any-file.html").(*external.Renderer)
|
||||
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
|
||||
|
||||
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
|
||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
|
||||
t.Run("RenderContentInIFrame", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
iframe := doc.Find("iframe")
|
||||
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
|
||||
|
||||
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
|
||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
|
||||
})
|
||||
}
|
||||
|
||||
@ -114,9 +114,16 @@ ENABLED = true
|
||||
[markup.html]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .html
|
||||
RENDER_COMMAND = `go run build/test-echo.go`
|
||||
IS_INPUT_FILE = false
|
||||
RENDER_CONTENT_MODE=sanitized
|
||||
RENDER_COMMAND = go run build/test-echo.go
|
||||
;RENDER_COMMAND = cat
|
||||
;IS_INPUT_FILE = true
|
||||
RENDER_CONTENT_MODE = sanitized
|
||||
|
||||
[markup.no-sanitizer]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .no-sanitizer
|
||||
RENDER_COMMAND = echo '<script>window.alert("hi")</script>'
|
||||
RENDER_CONTENT_MODE = no-sanitizer
|
||||
|
||||
[actions]
|
||||
ENABLED = true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user