-->
5 min read

Review: CVE-2026-35037

Table of Contents

Review

CVE: CVE-2026-35037

Name: Unauthenticated SSRF in GetWebsiteTitle allows access to internal services and cloud metadata

URL: https://github.com/advisories/GHSA-cqgf-f4x7-g6wc

Severity:

The Fix: https://github.com/lin-snow/Ech0/commit/4ca56fea5ba4cd477bd1c6134a0435014635b5d8

Project Description:

An open-source, self-hosted lightweight publishing platform for personal idea sharing.

The Bug

As stated by the security disclosure, the endpoint has no authentication, and no middleware is present either to ensure authentication:

appRouterGroup.PublicRouterGroup.GET("/website/title", h.CommonHandler.GetWebsiteTitle())

The handler doesn’t validate user input. No URL or host scheme validation is enforced via the DTO either:

func (commonHandler *CommonHandler) GetWebsiteTitle() gin.HandlerFunc {
    return res.Execute(func(ctx *gin.Context) res.Response {
        var dto commonModel.GetWebsiteTitleDto
        if err := ctx.ShouldBindQuery(&dto); err != nil { ... }
        title, err := commonHandler.commonService.GetWebsiteTitle(dto.WebSiteURL)
        ...
    })
}

An unrestricted outbound request is then executed:

client := &http.Client{
    Timeout: clientTimeout,
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,
        },
    },
}
req, err := http.NewRequest(method, url, nil)
...
resp, err := client.Do(req)

The Fix

#1 - Ensure the Endpoint is Authenticated

---:

appRouterGroup.PublicRouterGroup.GET("/website/title", h.CommonHandler.GetWebsiteTitle())

+++:

appRouterGroup.AuthRouterGroup.GET("/website/title", h.CommonHandler.GetWebsiteTitle())

#2 - Validate URLs & IPs

+++:

var blockedCIDRs = mustParseCIDRs(
	[]string{
		"0.0.0.0/8",
		"10.0.0.0/8",
		"100.64.0.0/10",
		"127.0.0.0/8",
		[TRUNCATED]
		}
		
[TRUNCATED]		
		
func isBlockedHostname(hostname string) bool {
	hostname = strings.ToLower(strings.TrimSpace(hostname))
	return hostname == "localhost" ||
		strings.HasSuffix(hostname, ".localhost") ||
		hostname == "host.docker.internal" ||
		hostname == "gateway.docker.internal"
}

[TRUNCATED]

func secureDialContext(timeout time.Duration) func(context.Context, string, string) (net.Conn, error) {
	dialer := &net.Dialer{Timeout: timeout}
	return func(ctx context.Context, network, address string) (net.Conn, error) {
		conn, err := dialer.DialContext(ctx, network, address)
		if err != nil {
			return nil, err
		}
		tcpAddr, ok := conn.RemoteAddr().(*net.TCPAddr)
		if !ok || tcpAddr == nil || isPrivateOrReservedIP(tcpAddr.IP) {
			_ = conn.Close()
			return nil, errors.New("连接目标地址不被允许")
		}
		return conn, nil
	}
}

[TRUNCATED]

#3 - Validate It With Tests

+++:

func TestValidatePublicHTTPURL(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		rawURL  string
		wantErr bool
	}{
		{
			name:    "allow_public_https_domain",
			rawURL:  "https://example.com",
			wantErr: false,
		},
		{
			name:    "allow_public_http_ip",
			rawURL:  "http://93.184.216.34",
			wantErr: false,
		},
		{
			name:    "reject_non_http_scheme",
			rawURL:  "ftp://example.com",
			wantErr: true,
		},
		{
			name:    "reject_loopback_ipv4",
			rawURL:  "http://127.0.0.1",
			wantErr: true,
		},
		{
			name:    "reject_loopback_ipv6",
			rawURL:  "http://[::1]",
			wantErr: true,
		},
		[TRUNCATED]

Lessons Learned

  • Always be aware of what endpoints are accessible to what groups. Ensure that some forms of tests or validation enforces a chosen state for endpoints, especially if they’re going to be unauthenticated.
  • Never trust user input, and always double-check user inputs multiple ways before performing sensitive operations like requests via any protocol/scheme (especially HTTP/HTTPS).

Outputting .md Report to PDF via Pandoc

Command Example:

pandoc index.md --lua-filter=keep-together.lua -V geometry:margin=1in -o post.pdf --pdf-engine=xelatex -V CJKmainfont="Arial Unicode MS" --pdf-engine-opt=-shell-escape

Claude-Created LUA:

-- keep-together.lua
-- Pandoc Lua filter: heading pagination, list item grouping, minted code blocks
--
-- Requirements:
--   pip3 install pygments
--   sudo tlmgr install needspace minted fvextra xcolor
--
-- Usage:
--   pandoc post.md \
--     --lua-filter=keep-together.lua \
--     --pdf-engine=xelatex \
--     -V geometry:margin=1in \
--     -V CJKmainfont="PingFang SC" \
--     --shell-escape \
--     -o post.pdf
--
-- Note: --shell-escape is required for minted to call Pygments.

------------------------------------------------------------------------
-- Inject LaTeX preamble packages
------------------------------------------------------------------------
function Meta(meta)
  local preamble = [[
\usepackage{needspace}
\usepackage{xcolor}
\usepackage{minted}

% minted style: friendly theme, grey background, line numbers
\setminted{
  bgcolor=gray!10,
  frame=single,
  framesep=4pt,
  rulecolor=gray,
  numbers=left,
  numbersep=8pt,
  fontsize=\small,
  breaklines=true,
  breakanywhere=false,
  tabsize=2,
  style=friendly,
}
]]

  local existing = meta['header-includes']
  local new_block = pandoc.RawBlock('latex', preamble)

  if existing == nil then
    meta['header-includes'] = pandoc.MetaBlocks({new_block})
  elseif existing.t == 'MetaBlocks' then
    table.insert(existing, new_block)
    meta['header-includes'] = existing
  elseif existing.t == 'MetaList' then
    table.insert(existing, pandoc.MetaBlocks({new_block}))
    meta['header-includes'] = existing
  else
    meta['header-includes'] = pandoc.MetaList({existing, pandoc.MetaBlocks({new_block})})
  end

  return meta
end

------------------------------------------------------------------------
-- Rule 1: Headings stay with following content via \needspace
------------------------------------------------------------------------
function Header(el)
  local lines = "6"
  if el.level == 3 then lines = "8" end
  if el.level == 2 then lines = "10" end

  return {
    pandoc.RawBlock('latex', '\\needspace{' .. lines .. '\\baselineskip}'),
    el,
    pandoc.RawBlock('latex', '\\nopagebreak[4]'),
  }
end

------------------------------------------------------------------------
-- Rule 2: Each ordered list item stays together on one page
------------------------------------------------------------------------
function OrderedList(el)
  local new_items = {}

  for _, item in ipairs(el.content) do
    local wrapped = {}
    table.insert(wrapped, pandoc.RawBlock('latex', '\\begin{minipage}{\\linewidth}'))
    for _, block in ipairs(item) do
      table.insert(wrapped, block)
    end
    table.insert(wrapped, pandoc.RawBlock('latex', '\\end{minipage}\\vspace{0.5em}'))
    table.insert(new_items, wrapped)
  end

  return pandoc.OrderedList(new_items, el.start, el.style, el.delimiter)
end

------------------------------------------------------------------------
-- Rule 3: Code blocks → minted with syntax highlighting
--
-- Key fix: build the entire block as ONE raw string so there is no
-- extra newline inserted between \begin{minted}{lang} and the first
-- line of code. A trailing newline before \end{minted} is intentional
-- and required by minted.
------------------------------------------------------------------------
function CodeBlock(el)
  -- Determine language; fall back to 'text' so minted never errors
  local lang = "text"
  if el.classes and el.classes[1] and el.classes[1] ~= "" then
    lang = el.classes[1]
  end

  -- Build the entire block as a single string — no inter-block gaps
  local block = '\\begin{minted}{' .. lang .. '}\n'
              .. el.text .. '\n'
              .. '\\end{minted}'

  return pandoc.RawBlock('latex', block)
end

------------------------------------------------------------------------
-- Rule 4: Preserve intentional blank lines / line breaks inside
-- regular paragraphs. Pandoc collapses soft line breaks by default.
-- This converts LineBreak inlines into explicit LaTeX newlines.
------------------------------------------------------------------------
function LineBreak()
  return pandoc.RawInline('latex', '\\\\\n')
end

alp1n3
Hi, I'm alp1n3

This is a collection of my cybersecurity notes & projects.

I graduated from Dakota State University with a MS in Cyber Defense & BS in Cyber Operations. Since then I've worked as a Malware Analyst with the U.S. Army Cyber Command, and am now a Web Application Security Consultant.

I'm a big fan of open security standards for applications and workflow automation when it comes to security testing. The easier it is to identify and replicate, the more secure everyone's apps can be! My other writings and projects are scattered across the web, but can be found in the links page.

Contact me:

Signal: alp1n3.01 | Email Me | GitHub


Content licenced under CC BY-NC-ND 4.0