Skip to content
Merged
3 changes: 3 additions & 0 deletions copi.owasp.org/assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ body {

main {
overflow-x: hidden;
}
.bg-clip-path-polygon {
clip-path: polygon(73.6% 51.7%, 91.7% 11.8%, 100% 46.4%, 97.4% 82.2%, 92.5% 84.9%, 75.7% 64%, 55.3% 47.5%, 46.5% 49.4%, 45% 62.9%, 50.3% 87.2%, 21.3% 64.1%, 0.1% 100%, 5.4% 51.1%, 21.4% 63.9%, 58.9% 0.2%, 73.6% 51.7%);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<rect width="100%" height="100%" stroke-width="0" fill="url(#983e3e4c-de6d-4c3f-8d64-b9761d1534cc)" />
</svg>
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
<div class="aspect-[1108/632] w-[69.25rem] bg-gradient-to-r from-[#80caff] to-[#4f46e5] opacity-20" style="clip-path: polygon(73.6% 51.7%, 91.7% 11.8%, 100% 46.4%, 97.4% 82.2%, 92.5% 84.9%, 75.7% 64%, 55.3% 47.5%, 46.5% 49.4%, 45% 62.9%, 50.3% 87.2%, 21.3% 64.1%, 0.1% 100%, 5.4% 51.1%, 21.4% 63.9%, 58.9% 0.2%, 73.6% 51.7%)"></div>
<div class="aspect-[1108/632] w-[69.25rem] bg-gradient-to-r from-[#80caff] to-[#4f46e5] opacity-20 bg-clip-path-polygon"></div>
</div>
<div class="mx-auto max-w-7xl px-6 pb-24 pt-10 sm:pb-40 lg:flex lg:px-8 lg:pt-40">
<div class="mx-auto max-w-2xl flex-shrink-0 lg:mx-0 lg:max-w-xl lg:pt-8">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<rect width="100%" height="100%" stroke-width="0" fill="url(#983e3e4c-de6d-4c3f-8d64-b9761d1534cc)" />
</svg>
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
<div class="aspect-[1108/632] w-[69.25rem] bg-gradient-to-r from-[#80caff] to-[#4f46e5] opacity-20" style="clip-path: polygon(73.6% 51.7%, 91.7% 11.8%, 100% 46.4%, 97.4% 82.2%, 92.5% 84.9%, 75.7% 64%, 55.3% 47.5%, 46.5% 49.4%, 45% 62.9%, 50.3% 87.2%, 21.3% 64.1%, 0.1% 100%, 5.4% 51.1%, 21.4% 63.9%, 58.9% 0.2%, 73.6% 51.7%)"></div>
<div class="aspect-[1108/632] w-[69.25rem] bg-gradient-to-r from-[#80caff] to-[#4f46e5] opacity-20 bg-clip-path-polygon"></div>
</div>
<div class="mx-auto max-w-7xl px-6 pb-24 pt-10 sm:pb-40 lg:flex lg:px-8 lg:pt-40">
<div class="mx-auto max-w-2xl flex-shrink-0 lg:mx-0 lg:max-w-xl lg:pt-8">
Expand Down Expand Up @@ -46,10 +46,6 @@
</div>
</div>
</div>




</main>

<!-- Footer -->
Expand All @@ -58,7 +54,6 @@
<div class="mx-auto max-w-7xl px-6 pb-8 pt-4 lg:px-8">
<div class="border-t border-white/10 pt-8 md:flex md:items-center md:justify-between">
<div class="flex space-x-6 md:order-2">

<a href="https://github.com/OWASP/cornucopia/tree/master/copi.owasp.org" class="text-gray-500 hover:text-gray-400">
<span class="sr-only">GitHub</span>
<svg class="h-8 w-8" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
Expand All @@ -67,7 +62,7 @@
</a>
<a href="https://cornucopia.owasp.org" class="opacity-70 hover:opacity-100" target="_blank" title="The OWASP Cornucopia home page" rel="external">
<span class="sr-only">OWASP Cornucopia</span>
<img src="/images/cornucopia_logo_white.svg" class="h-10" aria-hidden="true" />
<img src="/images/cornucopia_logo_white.svg" class="h-10" aria-hidden="true" />
</a>
</div>
<div>
Expand All @@ -81,4 +76,4 @@
</div>
</div>
</footer>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<rect width="100%" height="100%" stroke-width="0" fill="url(#983e3e4c-de6d-4c3f-8d64-b9761d1534cc)" />
</svg>
<div class="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]" aria-hidden="true">
<div class="aspect-[1108/632] w-[69.25rem] bg-gradient-to-r from-[#80caff] to-[#4f46e5] opacity-20" style="clip-path: polygon(73.6% 51.7%, 91.7% 11.8%, 100% 46.4%, 97.4% 82.2%, 92.5% 84.9%, 75.7% 64%, 55.3% 47.5%, 46.5% 49.4%, 45% 62.9%, 50.3% 87.2%, 21.3% 64.1%, 0.1% 100%, 5.4% 51.1%, 21.4% 63.9%, 58.9% 0.2%, 73.6% 51.7%)"></div>
<div class="aspect-[1108/632] w-[69.25rem] bg-gradient-to-r from-[#80caff] to-[#4f46e5] opacity-20 bg-clip-path-polygon"></div>
</div>

<div class="mx-auto max-w-7xl px-4 pb-24 pt-10 sm:px-6 sm:pb-40 lg:flex lg:px-8">
Expand Down
1 change: 1 addition & 0 deletions copi.owasp.org/lib/copi_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule CopiWeb.Endpoint do
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug CopiWeb.Plugs.LiveReloadSecurityHeadersPlug
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :copi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule CopiWeb.Plugs.LiveReloadSecurityHeadersPlug do
import Plug.Conn

alias CopiWeb.SecurityHeaders

def init(opts), do: opts

def call(conn, _opts) do
if live_reload_request?(conn) do
Enum.reduce(SecurityHeaders.live_reload_headers(), conn, fn {header, value}, acc ->
put_resp_header(acc, header, value)
end)
else
conn
end
end

defp live_reload_request?(%Plug.Conn{path_info: ["phoenix", "live_reload" | _]}), do: true
defp live_reload_request?(_conn), do: false
end
4 changes: 3 additions & 1 deletion copi.owasp.org/lib/copi_web/router.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
defmodule CopiWeb.Router do
use CopiWeb, :router

alias CopiWeb.SecurityHeaders

pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {CopiWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :put_secure_browser_headers, SecurityHeaders.browser_headers()
plug CopiWeb.Plugs.RateLimiterPlug
end

Expand Down
15 changes: 15 additions & 0 deletions copi.owasp.org/lib/copi_web/security_headers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule CopiWeb.SecurityHeaders do
def browser_headers do
%{
"content-security-policy" =>
"default-src 'self'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-src 'self'; font-src 'self'; media-src 'self'; object-src 'self'; manifest-src 'self'; worker-src 'self';"
}
end

def live_reload_headers do
%{
"content-security-policy" =>
"default-src 'self'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-src 'self'; font-src 'self'; media-src 'self'; object-src 'self'; manifest-src 'self'; worker-src 'self';"
}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule CopiWeb.SecurityHeadersTest do
use CopiWeb.ConnCase

alias CopiWeb.SecurityHeaders

test "browser pipeline includes form-action 'self' in CSP header", %{conn: conn} do
conn = get(conn, "/")
csp = get_resp_header(conn, "content-security-policy") |> List.first()
assert csp =~ "form-action 'self'"
end

test "browser headers come from SecurityHeaders.browser_headers/0", %{conn: _conn} do
csp = SecurityHeaders.browser_headers()["content-security-policy"]
assert csp =~ "form-action 'self'"
assert csp =~ "img-src 'self' data:"
end
end
33 changes: 22 additions & 11 deletions copi.owasp.org/test/copi_web/endpoint_test.exs
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
defmodule CopiWeb.EndpointTest do
use ExUnit.Case, async: true

import Plug.Conn
import Plug.Test

alias CopiWeb.Plugs.LiveReloadSecurityHeadersPlug
alias CopiWeb.SecurityHeaders

test "endpoint configuration includes connect_info for LiveView socket" do
# Get the endpoint configuration
config = CopiWeb.Endpoint.__sockets__()

# Find the /live socket configuration
live_socket = Enum.find(config, fn

live_socket = Enum.find(config, fn
{path, _, _} -> path == "/live"
_ -> false
end)

assert live_socket != nil, "LiveView socket should be configured"

{_path, _module, opts} = live_socket

# Check that websocket config includes connect_info

websocket_config = Keyword.get(opts, :websocket, [])
connect_info = Keyword.get(websocket_config, :connect_info, [])

# Verify connect_info is configured (it should have session and x_headers)

assert is_list(connect_info), "WebSocket should have connect_info configured"
assert length(connect_info) > 0, "WebSocket connect_info should not be empty"
end

test "endpoint is started and running" do
assert Process.whereis(CopiWeb.Endpoint) != nil
end
end

test "live reload requests receive the shared CSP headers" do
conn = conn(:get, "/phoenix/live_reload/frame")

conn = LiveReloadSecurityHeadersPlug.call(conn, LiveReloadSecurityHeadersPlug.init([]))

assert get_resp_header(conn, "content-security-policy") == [SecurityHeaders.live_reload_headers()["content-security-policy"]]
assert SecurityHeaders.live_reload_headers()["content-security-policy"] =~ "script-src 'self' 'unsafe-inline'"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule CopiWeb.Plugs.LiveReloadSecurityHeadersPlugTest do
use ExUnit.Case, async: true

import Plug.Conn
import Plug.Test

alias CopiWeb.Plugs.LiveReloadSecurityHeadersPlug
alias CopiWeb.SecurityHeaders

test "live reload request receives live reload CSP headers" do
conn = conn(:get, "/phoenix/live_reload/frame")
conn = LiveReloadSecurityHeadersPlug.call(conn, LiveReloadSecurityHeadersPlug.init([]))

assert get_resp_header(conn, "content-security-policy") ==
[SecurityHeaders.live_reload_headers()["content-security-policy"]]
end

test "live reload CSP allows unsafe-inline scripts" do
conn = conn(:get, "/phoenix/live_reload/frame")
conn = LiveReloadSecurityHeadersPlug.call(conn, LiveReloadSecurityHeadersPlug.init([]))

csp = get_resp_header(conn, "content-security-policy") |> List.first()
assert csp =~ "script-src 'self' 'unsafe-inline'"
end

test "non-live-reload request does not receive live reload CSP headers" do
conn = conn(:get, "/")
conn = LiveReloadSecurityHeadersPlug.call(conn, LiveReloadSecurityHeadersPlug.init([]))

assert get_resp_header(conn, "content-security-policy") == []
end

test "init/1 returns options unchanged" do
assert LiveReloadSecurityHeadersPlug.init([]) == []
assert LiveReloadSecurityHeadersPlug.init(foo: :bar) == [foo: :bar]
end

test "non-live-reload path is not affected by plug" do
conn = conn(:get, "/games/new")
conn = LiveReloadSecurityHeadersPlug.call(conn, LiveReloadSecurityHeadersPlug.init([]))

assert get_resp_header(conn, "content-security-policy") == []
end
end
25 changes: 19 additions & 6 deletions copi.owasp.org/test/copi_web/router_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,39 @@ defmodule CopiWeb.RouterTest do
assert html_response(conn, 200)
end

test "CSP header contains form-action 'self' on browser routes", %{conn: conn} do
conn = get(conn, "/")
csp = get_resp_header(conn, "content-security-policy") |> List.first()
assert csp =~ "form-action 'self'"
assert csp =~ "default-src 'self'"
assert csp =~ "img-src 'self' data:"
end

test "CSP header is present on multiple browser routes", %{conn: conn} do
for path <- ["/", "/cards", "/resources", "/privacy"] do
conn = get(conn, path)
csp = get_resp_header(conn, "content-security-policy") |> List.first()
assert csp =~ "form-action 'self'", "Missing CSP on #{path}"
end
end

describe "API routes" do
test "PUT /api/games/:game_id/players/:player_id/card requires valid params", %{conn: conn} do
# Create actual game and player with valid ULIDs
{:ok, game} = Copi.Cornucopia.create_game(%{name: "API Test", edition: "webapp"})
{:ok, player} = Copi.Cornucopia.create_player(%{name: "API Player", game_id: game.id})

conn = conn
|> put_req_header("accept", "application/json")
|> put("/api/games/#{game.id}/players/#{player.id}/card", %{"dealt_card_id" => "123"})

# Should respond (even if error due to invalid dealt_card_id)

assert conn.status in [200, 400, 403, 404, 406, 422, 500]
end
end

describe "LiveDashboard in test env" do
test "GET /dashboard", %{conn: conn} do
conn = get(conn, "/dashboard")
# LiveDashboard redirects to /dashboard/home
assert redirected_to(conn, 302) =~ "/dashboard/home"
end
end
end
end
Loading