Skip to content
Closed
4 changes: 4 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,10 @@ ROUTER = console
;; * 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
;;
;; When RENDER_CONTENT_MODE is iframe, these two options are available
;RENDER_CONTENT_IFRAME_SANDBOX="allow-scripts"
;RENDER_CONTENT_EXTERNAL_CSP="iframe-src 'self'; sandbox allow-scripts"

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
2 changes: 2 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,8 @@ IS_INPUT_FILE = 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_IFRAME_SANDBOX: **"allow-scripts"** When `RENDER_CONTENT_MODE` is `iframe`, this will be the allowed sandbox of iframe properties.
- RENDER_CONTENT_EXTERNAL_CSP: **"iframe-src 'self'; sandbox allow-scripts"** When `RENDER_CONTENT_MODE` is `iframe`, this will be the allowed CSP of external renderer response.

Two special environment variables are passed to the render command:

Expand Down
13 changes: 12 additions & 1 deletion modules/markup/external/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,25 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {

// SanitizerDisabled disabled sanitize if return true
func (p *Renderer) SanitizerDisabled() bool {
return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
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
}

// IframeSandbox represents iframe sandbox
func (p *Renderer) IframeSandbox() string {
return p.RenderContentIframeSandbox
}

// ExternalCSP represents external render CSP
func (p *Renderer) ExternalCSP() string {
return p.RenderContentExternalCSP
}

func envMark(envName string) string {
if runtime.GOOS == "windows" {
return "%" + envName + "%"
Expand Down
110 changes: 67 additions & 43 deletions modules/markup/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,22 @@ type Header struct {

// RenderContext represents a render context
type RenderContext struct {
Ctx context.Context
RelativePath string // relative path from tree root of the branch
Type string
IsWiki bool
URLPrefix string
Metas map[string]string
DefaultLink string
GitRepo *git.Repository
ShaExistCache map[string]bool
cancelFn func()
TableOfContents []Header
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
Ctx context.Context
RelativePath string // relative path from tree root of the branch
Type string
IsWiki bool
URLPrefix string
Metas map[string]string
DefaultLink string
GitRepo *git.Repository
ShaExistCache map[string]bool
cancelFn func()
TableOfContents []Header

// InStandalonePage is used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
// It is for maintenance and security purpose, to avoid rendering external JS into embedded page unexpectedly.
// The caller of the Render must set security headers correctly before setting it to true
InStandalonePage bool
}

// Cancel runs any cleanup functions that have been registered for this Ctx
Expand Down Expand Up @@ -99,13 +103,19 @@ type PostProcessRenderer interface {
NeedPostProcess() bool
}

// PostProcessRenderer defines an interface for external renderers
// ExternalRenderer 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

// IframeSandbox represents iframe sandbox attribute for the <iframe> tag
IframeSandbox() string

// ExternalCSP represents the Content-Security-Policy header for external render
ExternalCSP() string
}

// RendererContentDetector detects if the content can be rendered
Expand Down Expand Up @@ -152,14 +162,41 @@ func DetectRendererType(filename string, input io.Reader) string {
return ""
}

// GetRenderer returned the renderer according type or relative path
func GetRenderer(renderType, relativePath string) (Renderer, error) {
if renderType != "" {
if renderer, ok := renderers[renderType]; ok {
return renderer, nil
}
// FIXME: is it correct? if it returns here, then relativePath won't take effect
return nil, ErrUnsupportedRenderType{renderType}
}

if relativePath != "" {
extension := strings.ToLower(filepath.Ext(relativePath))
if renderer, ok := extRenderers[extension]; ok {
return renderer, nil
}
return nil, ErrUnsupportedRenderExtension{extension}
}

return nil, errors.New("render options both filename and type missing")
}

// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
if ctx.Type != "" {
return renderByType(ctx, input, output)
} else if ctx.RelativePath != "" {
return renderFile(ctx, input, output)
renderer, err := GetRenderer(ctx.Type, ctx.RelativePath)
if err != nil {
return err
}

if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
// 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, r.IframeSandbox())
}
return errors.New("Render options both filename and type missing")

return RenderDirect(ctx, renderer, input, output)
}

// RenderString renders Markup string to HTML with all specific handling stuff and return string
Expand All @@ -177,7 +214,7 @@ type nopCloser struct {

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

func renderIFrame(ctx *RenderContext, output io.Writer) error {
func renderIFrame(ctx *RenderContext, output io.Writer, iframeSandbox string) 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
Expand All @@ -187,18 +224,27 @@ func renderIFrame(ctx *RenderContext, output io.Writer) error {
name="giteaExternalRender"
onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
sandbox="allow-scripts"
sandbox="%s"
></iframe>`,
setting.AppSubURL,
url.PathEscape(ctx.Metas["user"]),
url.PathEscape(ctx.Metas["repo"]),
ctx.Metas["BranchNameSubURL"],
url.PathEscape(ctx.RelativePath),
iframeSandbox,
))
return err
}

func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
// RenderDirect renders markup file to HTML with all specific handling stuff.
func RenderDirect(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
// to prevent from rendering external JS into embedded page unexpectedly, which would lead to XSS attack
if !ctx.InStandalonePage {
return errors.New("external render with iframe can only render in standalone page")
}
}

var wg sync.WaitGroup
var err error
pr, pw := io.Pipe()
Expand Down Expand Up @@ -262,13 +308,6 @@ func (err ErrUnsupportedRenderType) Error() string {
return fmt.Sprintf("Unsupported render type: %s", err.Type)
}

func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error {
if renderer, ok := renderers[ctx.Type]; ok {
return render(ctx, renderer, input, output)
}
return ErrUnsupportedRenderType{ctx.Type}
}

// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
type ErrUnsupportedRenderExtension struct {
Extension string
Expand All @@ -278,21 +317,6 @@ func (err ErrUnsupportedRenderExtension) Error() string {
return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
}

func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
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 ErrUnsupportedRenderExtension{extension}
}

// Type returns if markup format via the filename
func Type(filename string) string {
if parser := GetRendererByFileName(filename); parser != nil {
Expand Down
35 changes: 20 additions & 15 deletions modules/setting/markup.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ const (

// MarkupRenderer defines the external parser configured in ini
type MarkupRenderer struct {
Enabled bool
MarkupName string
Command string
FileExtensions []string
IsInputFile bool
NeedPostProcess bool
MarkupSanitizerRules []MarkupSanitizerRule
RenderContentMode string
Enabled bool
MarkupName string
Command string
FileExtensions []string
IsInputFile bool
NeedPostProcess bool
MarkupSanitizerRules []MarkupSanitizerRule
RenderContentMode string
RenderContentIframeSandbox string
RenderContentExternalCSP string
}

// MarkupSanitizerRule defines the policy for whitelisting attributes on
Expand Down Expand Up @@ -158,6 +160,7 @@ func newMarkupRenderer(name string, sec *ini.Section) {
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 {
Expand All @@ -166,12 +169,14 @@ func newMarkupRenderer(name string, sec *ini.Section) {
}

ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
Enabled: sec.Key("ENABLED").MustBool(false),
MarkupName: name,
FileExtensions: exts,
Command: command,
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
RenderContentMode: renderContentMode,
Enabled: sec.Key("ENABLED").MustBool(false),
MarkupName: name,
FileExtensions: exts,
Command: command,
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
RenderContentMode: renderContentMode,
RenderContentIframeSandbox: sec.Key("RENDER_CONTENT_IFRAME_SANDBOX").MustString("allow-scripts"),
RenderContentExternalCSP: sec.Key("RENDER_CONTENT_EXTERNAL_CSP").MustString("iframe-src 'self'; sandbox allow-scripts"),
})
}
51 changes: 22 additions & 29 deletions routers/web/repo/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
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
// RenderFile uses an external render to render a file by repos path
func RenderFile(ctx *context.Context) {
blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
if err != nil {
Expand All @@ -37,43 +33,40 @@ func RenderFile(ctx *context.Context) {
}
defer dataRc.Close()

buf := make([]byte, 1024)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]

st := typesniffer.DetectContentType(buf)
isTextFile := st.IsText()
treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}

rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
renderer, err := markup.GetRenderer("", ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetRenderer", err)
return
}

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")
externalRender, ok := renderer.(markup.ExternalRenderer)
if !ok {
ctx.Error(http.StatusBadRequest, "External render only")
return
}

treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
externalCSP := externalRender.ExternalCSP()
if externalCSP == "" {
ctx.Error(http.StatusBadRequest, "External render must have valid Content-Security-Header")
return
}

ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
err = markup.Render(&markup.RenderContext{
ctx.Resp.Header().Add("Content-Security-Policy", externalCSP)

if err = markup.RenderDirect(&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)
}, renderer, dataRc, ctx.Resp); err != nil {
ctx.ServerError("RenderDirect", err)
return
}
}