mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-11 19:18:46 +00:00
Compare commits
1 Commits
v1.23.0
...
Xe/remove-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b193a33a26 |
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: Homebrew/actions/setup-homebrew@main
|
uses: Homebrew/actions/setup-homebrew@main
|
||||||
|
|
||||||
- name: Setup Homebrew cellar cache
|
- name: Setup Homebrew cellar cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/home/linuxbrew/.linuxbrew/Cellar
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
uses: Homebrew/actions/setup-homebrew@main
|
uses: Homebrew/actions/setup-homebrew@main
|
||||||
|
|
||||||
- name: Setup Homebrew cellar cache
|
- name: Setup Homebrew cellar cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/home/linuxbrew/.linuxbrew/Cellar
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
brew bundle
|
brew bundle
|
||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
2
.github/workflows/docs-deploy.yml
vendored
2
.github/workflows/docs-deploy.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: techarohq
|
username: techarohq
|
||||||
|
|||||||
6
.github/workflows/go.yml
vendored
6
.github/workflows/go.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
uses: Homebrew/actions/setup-homebrew@main
|
uses: Homebrew/actions/setup-homebrew@main
|
||||||
|
|
||||||
- name: Setup Homebrew cellar cache
|
- name: Setup Homebrew cellar cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/home/linuxbrew/.linuxbrew/Cellar
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
brew bundle
|
brew bundle
|
||||||
|
|
||||||
- name: Setup Golang caches
|
- name: Setup Golang caches
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
${{ runner.os }}-golang-
|
${{ runner.os }}-golang-
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
4
.github/workflows/package-builds-stable.yml
vendored
4
.github/workflows/package-builds-stable.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
uses: Homebrew/actions/setup-homebrew@main
|
uses: Homebrew/actions/setup-homebrew@main
|
||||||
|
|
||||||
- name: Setup Homebrew cellar cache
|
- name: Setup Homebrew cellar cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/home/linuxbrew/.linuxbrew/Cellar
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
brew bundle
|
brew bundle
|
||||||
|
|
||||||
- name: Setup Golang caches
|
- name: Setup Golang caches
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
uses: Homebrew/actions/setup-homebrew@main
|
uses: Homebrew/actions/setup-homebrew@main
|
||||||
|
|
||||||
- name: Setup Homebrew cellar cache
|
- name: Setup Homebrew cellar cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/home/linuxbrew/.linuxbrew/Cellar
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
brew bundle
|
brew bundle
|
||||||
|
|
||||||
- name: Setup Golang caches
|
- name: Setup Golang caches
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go tool yeet
|
go tool yeet
|
||||||
|
|
||||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: packages
|
name: packages
|
||||||
path: var/*
|
path: var/*
|
||||||
|
|||||||
4
.github/workflows/smoke-tests.yml
vendored
4
.github/workflows/smoke-tests.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
node-version: latest
|
node-version: latest
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV
|
run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: ${{ env.ARTIFACT_NAME }}
|
name: ${{ env.ARTIFACT_NAME }}
|
||||||
|
|||||||
2
.github/workflows/ssh-ci-runner-cron.yml
vendored
2
.github/workflows/ssh-ci-runner-cron.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
run: uvx zizmor --format sarif . > results.sarif
|
run: uvx zizmor --format sarif . > results.sarif
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Allow multiple consecutive slashes in a row in application paths ([#754](https://github.com/TecharoHQ/anubis/issues/754)).
|
- Allow multiple consecutive slashes in a row in application paths ([#754](https://github.com/TecharoHQ/anubis/issues/754)).
|
||||||
- Add option to set `targetSNI` to special keyword 'auto' to indicate that it should be automatically set to the request Host name ([424](https://github.com/TecharoHQ/anubis/issues/424)).
|
- Add option to set `targetSNI` to special keyword 'auto' to indicate that it should be automatically set to the request Host name ([424](https://github.com/TecharoHQ/anubis/issues/424)).
|
||||||
- The Preact challenge has been removed from the default configuration. It will be deprecated in the future.
|
- The Preact challenge has been removed from the default configuration. It will be deprecated in the future.
|
||||||
- An open redirect when in subrequest mode has been fixed.
|
|
||||||
|
|
||||||
### Potentially breaking changes
|
### Potentially breaking changes
|
||||||
|
|
||||||
|
|||||||
31
lib/http.go
31
lib/http.go
@@ -296,16 +296,6 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
|
|||||||
if proto == "" || host == "" || uri == "" {
|
if proto == "" || host == "" || uri == "" {
|
||||||
return "", errors.New(localizer.T("missing_required_forwarded_headers"))
|
return "", errors.New(localizer.T("missing_required_forwarded_headers"))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch proto {
|
|
||||||
case "http", "https":
|
|
||||||
// allowed
|
|
||||||
default:
|
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
|
||||||
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
|
|
||||||
return "", errors.New(localizer.T("invalid_redirect"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
||||||
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg := internal.GetRequestLogger(s.logger, r)
|
||||||
@@ -379,31 +369,16 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
localizer := localization.GetLocalizer(r)
|
localizer := localization.GetLocalizer(r)
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
urlParsed, err := url.ParseRequestURI(redir)
|
urlParsed, err := r.URL.Parse(redir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if ParseRequestURI fails, try as relative URL
|
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
|
||||||
urlParsed, err = r.URL.Parse(redir)
|
|
||||||
if err != nil {
|
|
||||||
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate URL scheme to prevent javascript:, data:, file:, tel:, etc.
|
|
||||||
switch urlParsed.Scheme {
|
|
||||||
case "", "http", "https":
|
|
||||||
// allowed: empty scheme means relative URL
|
|
||||||
default:
|
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
|
||||||
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
|
|
||||||
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hostNotAllowed := len(urlParsed.Host) > 0 &&
|
hostNotAllowed := len(urlParsed.Host) > 0 &&
|
||||||
len(s.opts.RedirectDomains) != 0 &&
|
len(s.opts.RedirectDomains) != 0 &&
|
||||||
!matchRedirectDomain(s.opts.RedirectDomains, urlParsed.Host)
|
!matchRedirectDomain(s.opts.RedirectDomains, urlParsed.Host)
|
||||||
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
|
hostMismatch := r.URL.Host != "" && urlParsed.Host != r.URL.Host
|
||||||
|
|
||||||
if hostNotAllowed || hostMismatch {
|
if hostNotAllowed || hostMismatch {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg := internal.GetRequestLogger(s.logger, r)
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRedirectSecurity(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
testType string // "constructRedirectURL", "serveHTTPNext", "renderIndex"
|
|
||||||
|
|
||||||
// For constructRedirectURL tests
|
|
||||||
xForwardedProto string
|
|
||||||
xForwardedHost string
|
|
||||||
xForwardedUri string
|
|
||||||
|
|
||||||
// For serveHTTPNext tests
|
|
||||||
redirParam string
|
|
||||||
reqHost string
|
|
||||||
|
|
||||||
// For renderIndex tests
|
|
||||||
returnHTTPStatusOnly bool
|
|
||||||
|
|
||||||
// Expected results
|
|
||||||
expectedStatus int
|
|
||||||
shouldError bool
|
|
||||||
shouldNotRedirect bool
|
|
||||||
shouldBlock bool
|
|
||||||
errorContains string
|
|
||||||
}{
|
|
||||||
// constructRedirectURL tests - X-Forwarded-Proto validation
|
|
||||||
{
|
|
||||||
name: "constructRedirectURL: javascript protocol should be rejected",
|
|
||||||
testType: "constructRedirectURL",
|
|
||||||
xForwardedProto: "javascript",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "alert(1)",
|
|
||||||
shouldError: true,
|
|
||||||
errorContains: "invalid",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "constructRedirectURL: data protocol should be rejected",
|
|
||||||
testType: "constructRedirectURL",
|
|
||||||
xForwardedProto: "data",
|
|
||||||
xForwardedHost: "text/html",
|
|
||||||
xForwardedUri: ",<script>alert(1)</script>",
|
|
||||||
shouldError: true,
|
|
||||||
errorContains: "invalid",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "constructRedirectURL: file protocol should be rejected",
|
|
||||||
testType: "constructRedirectURL",
|
|
||||||
xForwardedProto: "file",
|
|
||||||
xForwardedHost: "",
|
|
||||||
xForwardedUri: "/etc/passwd",
|
|
||||||
shouldError: true,
|
|
||||||
errorContains: "invalid",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "constructRedirectURL: ftp protocol should be rejected",
|
|
||||||
testType: "constructRedirectURL",
|
|
||||||
xForwardedProto: "ftp",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "/file.txt",
|
|
||||||
shouldError: true,
|
|
||||||
errorContains: "invalid",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "constructRedirectURL: https protocol should be allowed",
|
|
||||||
testType: "constructRedirectURL",
|
|
||||||
xForwardedProto: "https",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "/foo",
|
|
||||||
shouldError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "constructRedirectURL: http protocol should be allowed",
|
|
||||||
testType: "constructRedirectURL",
|
|
||||||
xForwardedProto: "http",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "/bar",
|
|
||||||
shouldError: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// serveHTTPNext tests - redir parameter validation
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: javascript: URL should be rejected",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "javascript:alert(1)",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
shouldNotRedirect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: data: URL should be rejected",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "data:text/html,<script>alert(1)</script>",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
shouldNotRedirect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: file: URL should be rejected",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "file:///etc/passwd",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
shouldNotRedirect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: vbscript: URL should be rejected",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "vbscript:msgbox(1)",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
shouldNotRedirect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: valid https URL should work",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "https://example.com/foo",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusFound,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: valid relative URL should work",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "/foo/bar",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusFound,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: external domain should be blocked",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "https://evil.com/phishing",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
shouldBlock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: relative path should work",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "/safe/path",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusFound,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "serveHTTPNext: empty redir should show success page",
|
|
||||||
testType: "serveHTTPNext",
|
|
||||||
redirParam: "",
|
|
||||||
reqHost: "example.com",
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
|
|
||||||
// renderIndex tests - full subrequest auth flow
|
|
||||||
{
|
|
||||||
name: "renderIndex: javascript protocol in X-Forwarded-Proto",
|
|
||||||
testType: "renderIndex",
|
|
||||||
xForwardedProto: "javascript",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "alert(1)",
|
|
||||||
returnHTTPStatusOnly: true,
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "renderIndex: data protocol in X-Forwarded-Proto",
|
|
||||||
testType: "renderIndex",
|
|
||||||
xForwardedProto: "data",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "text/html,<script>alert(1)</script>",
|
|
||||||
returnHTTPStatusOnly: true,
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "renderIndex: valid https redirect",
|
|
||||||
testType: "renderIndex",
|
|
||||||
xForwardedProto: "https",
|
|
||||||
xForwardedHost: "example.com",
|
|
||||||
xForwardedUri: "/protected/page",
|
|
||||||
returnHTTPStatusOnly: true,
|
|
||||||
expectedStatus: http.StatusTemporaryRedirect,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
opts: Options{
|
|
||||||
PublicUrl: "https://anubis.example.com",
|
|
||||||
RedirectDomains: []string{},
|
|
||||||
},
|
|
||||||
logger: slog.Default(),
|
|
||||||
policy: &policy.ParsedConfig{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
switch tt.testType {
|
|
||||||
case "constructRedirectURL":
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
req.Header.Set("X-Forwarded-Proto", tt.xForwardedProto)
|
|
||||||
req.Header.Set("X-Forwarded-Host", tt.xForwardedHost)
|
|
||||||
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
|
||||||
|
|
||||||
redirectURL, err := s.constructRedirectURL(req)
|
|
||||||
|
|
||||||
if tt.shouldError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error containing %q, got nil", tt.errorContains)
|
|
||||||
t.Logf("got redirect URL: %s", redirectURL)
|
|
||||||
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
|
|
||||||
t.Logf("expected error containing %q, got: %v", tt.errorContains, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got: %v", err)
|
|
||||||
}
|
|
||||||
// Verify the redirect URL is safe
|
|
||||||
if redirectURL != "" {
|
|
||||||
parsed, err := url.Parse(redirectURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to parse redirect URL: %v", err)
|
|
||||||
}
|
|
||||||
redirParam := parsed.Query().Get("redir")
|
|
||||||
if redirParam != "" {
|
|
||||||
redirParsed, err := url.Parse(redirParam)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to parse redir parameter: %v", err)
|
|
||||||
}
|
|
||||||
if redirParsed.Scheme != "http" && redirParsed.Scheme != "https" {
|
|
||||||
t.Errorf("redir parameter has unsafe scheme: %s", redirParsed.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "serveHTTPNext":
|
|
||||||
req := httptest.NewRequest("GET", "/.within.website/?redir="+url.QueryEscape(tt.redirParam), nil)
|
|
||||||
req.Host = tt.reqHost
|
|
||||||
req.URL.Host = tt.reqHost
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
s.ServeHTTPNext(rr, req)
|
|
||||||
|
|
||||||
if rr.Code != tt.expectedStatus {
|
|
||||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
|
|
||||||
t.Logf("body: %s", rr.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.shouldNotRedirect {
|
|
||||||
location := rr.Header().Get("Location")
|
|
||||||
if location != "" {
|
|
||||||
t.Errorf("expected no redirect, but got Location header: %s", location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.shouldBlock {
|
|
||||||
location := rr.Header().Get("Location")
|
|
||||||
if location != "" && strings.Contains(location, "evil.com") {
|
|
||||||
t.Errorf("redirect to evil.com was not blocked: %s", location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "renderIndex":
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
req.Header.Set("X-Forwarded-Proto", tt.xForwardedProto)
|
|
||||||
req.Header.Set("X-Forwarded-Host", tt.xForwardedHost)
|
|
||||||
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
s.RenderIndex(rr, req, policy.CheckResult{}, nil, tt.returnHTTPStatusOnly)
|
|
||||||
|
|
||||||
if rr.Code != tt.expectedStatus {
|
|
||||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.expectedStatus == http.StatusTemporaryRedirect {
|
|
||||||
location := rr.Header().Get("Location")
|
|
||||||
if location == "" {
|
|
||||||
t.Error("expected Location header, got none")
|
|
||||||
} else {
|
|
||||||
// Verify the location doesn't contain javascript:
|
|
||||||
if strings.Contains(location, "javascript") {
|
|
||||||
t.Errorf("Location header contains 'javascript': %s", location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@techaro/anubis",
|
"name": "@techaro/anubis",
|
||||||
"version": "1.23.0",
|
"version": "1.23.0-pre1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@techaro/anubis",
|
"name": "@techaro/anubis",
|
||||||
"version": "1.23.0",
|
"version": "1.23.0-pre1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-crypto/sha256-js": "^5.2.0",
|
"@aws-crypto/sha256-js": "^5.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@techaro/anubis",
|
"name": "@techaro/anubis",
|
||||||
"version": "1.23.0",
|
"version": "1.23.0-pre1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -12,37 +12,39 @@ spec:
|
|||||||
app: nginx-external-auth
|
app: nginx-external-auth
|
||||||
spec:
|
spec:
|
||||||
volumes:
|
volumes:
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: nginx-cfg
|
name: nginx-cfg
|
||||||
containers:
|
containers:
|
||||||
- name: www
|
- name: www
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "128Mi"
|
memory: "128Mi"
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
requests:
|
requests:
|
||||||
memory: "128Mi"
|
memory: "128Mi"
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/nginx/conf.d
|
mountPath: /etc/nginx/conf.d
|
||||||
readOnly: true
|
readOnly: true
|
||||||
- name: anubis
|
- name: anubis
|
||||||
image: ttl.sh/techaro/anubis:latest
|
image: ttl.sh/techaro/anubis-external-auth:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 500m
|
||||||
memory: 128Mi
|
memory: 128Mi
|
||||||
requests:
|
requests:
|
||||||
cpu: 250m
|
cpu: 250m
|
||||||
memory: 128Mi
|
memory: 128Mi
|
||||||
env:
|
env:
|
||||||
- name: TARGET
|
- name: TARGET
|
||||||
value: " "
|
value: " "
|
||||||
- name: REDIRECT_DOMAINS
|
- name: REDIRECT_DOMAINS
|
||||||
value: nginx.local.cetacean.club
|
value: nginx.local.cetacean.club
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- nginx.local.cetacean.club
|
- nginx.local.cetacean.club
|
||||||
secretName: nginx-local-cetacean-club-public-tls
|
secretName: nginx-local-cetacean-club-public-tls
|
||||||
rules:
|
rules:
|
||||||
- host: nginx.local.cetacean.club
|
- host: nginx.local.cetacean.club
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- pathType: Prefix
|
- pathType: Prefix
|
||||||
path: "/"
|
path: "/"
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: nginx-external-auth
|
name: nginx-external-auth
|
||||||
port:
|
port:
|
||||||
name: http
|
name: http
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ configMapGenerator:
|
|||||||
- name: nginx-cfg
|
- name: nginx-cfg
|
||||||
behavior: create
|
behavior: create
|
||||||
files:
|
files:
|
||||||
- ./conf.d/default.conf
|
- ./conf.d/default.conf
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ spec:
|
|||||||
selector:
|
selector:
|
||||||
app: nginx-external-auth
|
app: nginx-external-auth
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
port: 80
|
port: 80
|
||||||
targetPort: 80
|
targetPort: 80
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Build container image
|
# Build container image
|
||||||
(
|
(
|
||||||
cd ../.. &&
|
cd ../.. \
|
||||||
npm ci &&
|
&& npm ci \
|
||||||
npm run container -- \
|
&& npm run container -- \
|
||||||
--docker-repo ttl.sh/techaro/anubis \
|
--docker-repo ttl.sh/techaro/anubis-external-auth \
|
||||||
--docker-tags ttl.sh/techaro/anubis:latest
|
--docker-tags ttl.sh/techaro/anubis-external-auth:latest
|
||||||
)
|
)
|
||||||
|
|
||||||
kubectl apply -k .
|
kubectl apply -k .
|
||||||
echo "open https://nginx.local.cetacean.club, press control c when done"
|
echo "open https://nginx.local.cetacean.club, press control c when done"
|
||||||
|
|
||||||
control_c() {
|
control_c() {
|
||||||
kubectl delete -k .
|
kubectl delete -k .
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
trap control_c SIGINT
|
trap control_c SIGINT
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user