mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-25 09:34:29 +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.
|
;; * 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.
|
;; * 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
|
;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/markup"
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/kballard/go-shellquote"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterRenderers registers all supported third part renderers according settings
|
// 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_SRC"), baseLinkSrc,
|
||||||
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
|
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
|
||||||
).Replace(p.Command)
|
).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:]
|
args := commands[1:]
|
||||||
|
|
||||||
if p.IsInputFile {
|
if p.IsInputFile {
|
||||||
|
|||||||
@ -120,31 +120,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders markup file to HTML with all specific handling stuff.
|
// FindRendererByContext finds renderer by RenderContext
|
||||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
// 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 != "" {
|
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
|
||||||
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
|
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
|
||||||
if ctx.RenderOptions.MarkupType == "" {
|
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]
|
renderer := renderers[ctx.RenderOptions.MarkupType]
|
||||||
if renderer == nil {
|
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 != "" {
|
return renderer, nil
|
||||||
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 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
|
// 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
|
ctx.usedByRender = true
|
||||||
if ctx.RenderHelper != nil {
|
if ctx.RenderHelper != nil {
|
||||||
defer ctx.RenderHelper.CleanUp()
|
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) {
|
eg.Go(func() (err error) {
|
||||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
if RendererNeedPostProcess(renderer) {
|
||||||
err = PostProcessDefault(ctx, pr1, pw2)
|
err = PostProcessDefault(ctx, pr1, pw2)
|
||||||
} else {
|
} else {
|
||||||
_, err = io.Copy(pw2, pr1)
|
_, err = io.Copy(pw2, pr1)
|
||||||
|
|||||||
@ -259,7 +259,9 @@ func newMarkupRenderer(name string, sec ConfigSection) {
|
|||||||
FileExtensions: exts,
|
FileExtensions: exts,
|
||||||
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),
|
|
||||||
RenderContentMode: renderContentMode,
|
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) {
|
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()
|
markupRd, markupWr := io.Pipe()
|
||||||
defer markupWr.Close()
|
defer markupWr.Close()
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
// We allow NBSP here this is rendered
|
if markup.RendererNeedPostProcess(renderer) {
|
||||||
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
|
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())
|
output = template.HTML(sb.String())
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
err = markup.Render(renderCtx, input, markupWr)
|
|
||||||
|
err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr)
|
||||||
_ = markupWr.CloseWithError(err)
|
_ = markupWr.CloseWithError(err)
|
||||||
<-done
|
<-done
|
||||||
return escaped, output, err
|
return escaped, output, err
|
||||||
|
|||||||
@ -4,18 +4,23 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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"
|
||||||
"code.gitea.io/gitea/modules/markup/external"
|
"code.gitea.io/gitea/modules/markup/external"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExternalMarkupRenderer(t *testing.T) {
|
func TestExternalMarkupRenderer(t *testing.T) {
|
||||||
@ -25,36 +30,52 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RenderContentDirectly", func(t *testing.T) {
|
||||||
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
|
|
||||||
bs, err := io.ReadAll(resp.Body)
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
doc := NewHTMLParser(t, bytes.NewBuffer(bs))
|
|
||||||
div := doc.Find("div.file-view")
|
div := doc.Find("div.file-view")
|
||||||
data, err := div.Html()
|
data, err := div.Html()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
||||||
|
})
|
||||||
|
|
||||||
r := markup.GetRendererByFileName("a.html").(*external.Renderer)
|
r := markup.GetRendererByFileName("any-file.html").(*external.Renderer)
|
||||||
r.RenderContentMode = setting.RenderContentModeIframe
|
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
|
||||||
|
|
||||||
req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
t.Run("RenderContentInIFrame", func(t *testing.T) {
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
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"))
|
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
bs, err = io.ReadAll(resp.Body)
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
assert.NoError(t, err)
|
|
||||||
doc = NewHTMLParser(t, bytes.NewBuffer(bs))
|
|
||||||
iframe := doc.Find("iframe")
|
iframe := doc.Find("iframe")
|
||||||
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
|
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
|
||||||
|
|
||||||
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
|
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
bs, err = io.ReadAll(resp.Body)
|
bs, err := io.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
|
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))
|
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,9 +114,16 @@ ENABLED = true
|
|||||||
[markup.html]
|
[markup.html]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
FILE_EXTENSIONS = .html
|
FILE_EXTENSIONS = .html
|
||||||
RENDER_COMMAND = `go run build/test-echo.go`
|
RENDER_COMMAND = go run build/test-echo.go
|
||||||
IS_INPUT_FILE = false
|
;RENDER_COMMAND = cat
|
||||||
RENDER_CONTENT_MODE=sanitized
|
;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]
|
[actions]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user