Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e49c46f
docs: make GitHub-first handoff rules binding
hotzenplotz5 Aug 4, 2026
21b102d
security: derive series artwork scope from route
hotzenplotz5 Aug 4, 2026
7effa76
security: apply route scope before authorization
hotzenplotz5 Aug 4, 2026
d9ff695
test(security): cover series artwork route scope
hotzenplotz5 Aug 4, 2026
548bd37
build: add series artwork route scope security test
hotzenplotz5 Aug 4, 2026
6304662
fix(security): preserve canonical gate invocation
hotzenplotz5 Aug 4, 2026
e453b1d
docs: mirror binding GitHub-first handoff rules
hotzenplotz5 Aug 4, 2026
a8bbdc4
docs: record route-derived settings security scope
hotzenplotz5 Aug 4, 2026
d0eda7f
docs: refresh current route-scope hardening state
hotzenplotz5 Aug 4, 2026
bf4a89a
docs: refresh route-scope handoff state
hotzenplotz5 Aug 4, 2026
6af08f0
docs: synchronize route-scope project status
hotzenplotz5 Aug 4, 2026
03fa8fa
docs: require local build test install commands
hotzenplotz5 Aug 4, 2026
25ec5c6
docs: mirror required local command block
hotzenplotz5 Aug 4, 2026
c76ba58
test: enforce final local command handoff
hotzenplotz5 Aug 4, 2026
c75d202
docs: forbid invented context and irrelevant CI waits
hotzenplotz5 Aug 4, 2026
536f123
test: enforce no-invention and relevant-CI rules
hotzenplotz5 Aug 4, 2026
ac2f61a
docs: preserve handoff context while adding rules
hotzenplotz5 Aug 4, 2026
1002b86
test: preserve permanent shell and CI safety rules
hotzenplotz5 Aug 4, 2026
63ad438
docs: make operational safety invariants permanent
hotzenplotz5 Aug 4, 2026
9da3177
docs: mirror permanent operational safety invariants
hotzenplotz5 Aug 4, 2026
0f18199
docs(handoff): require continuous status updates
hotzenplotz5 Aug 4, 2026
a8bb202
test(handoff): guard continuous status updates
hotzenplotz5 Aug 4, 2026
6323b49
docs(handoff): require complete new-chat repository orientation
hotzenplotz5 Aug 4, 2026
8b54eba
test(handoff): guard complete repository orientation
hotzenplotz5 Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions core/http/src/TestHttpServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ApiRouter.h"
#include "BrowserSessionCsrfRecoveryService.h"
#include "SecurityConfiguration.h"
#include "SeriesArtworkSettingsSecurityRequest.h"

#include <cstdlib>
#include <fstream>
Expand Down Expand Up @@ -299,8 +300,13 @@ HttpServerResponse TestHttpServer::handleRequest(
std::move(response));
}

const SecurityGateDecision gate =
securityHttpGate_->evaluate(request);
const HttpServerRequest securityRequest =
SeriesArtworkSettingsSecurityRequest::forAuthorization(request);
SecurityGateDecision gate;
{
const HttpServerRequest& request = securityRequest;
gate = securityHttpGate_->evaluate(request);
}

if (!gate.allowed)
{
Expand Down Expand Up @@ -369,4 +375,4 @@ HttpServerResponse TestHttpServer::mapApiResponse(
response.headers["Content-Type"] = contentType;
response.body = body;
return response;
}
}
97 changes: 97 additions & 0 deletions core/security/include/SeriesArtworkSettingsSecurityRequest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#pragma once

#include "HttpServerRequest.h"

#include <algorithm>
#include <string>

namespace SeriesArtworkSettingsSecurityRequest
{
struct RouteScope
{
bool matched = false;
std::string backendId;
};

inline bool validBackendId(const std::string& backendId)
{
return !backendId.empty() && backendId.size() <= 128U &&
std::all_of(
backendId.begin(),
backendId.end(),
[](unsigned char character)
{
return (character >= 'a' && character <= 'z') ||
(character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9') ||
character == '-' || character == '_' || character == '.';
});
}

inline RouteScope routeScope(const std::string& target)
{
static const std::string prefix = "/api/backends/";
static const std::string suffix = "/settings/series-artwork";

const std::size_t query = target.find('?');
const std::string path = query == std::string::npos
? target
: target.substr(0, query);

RouteScope scope;
if (path.size() < prefix.size() + suffix.size() ||
path.compare(0, prefix.size(), prefix) != 0 ||
path.compare(
path.size() - suffix.size(),
suffix.size(),
suffix) != 0)
{
return scope;
}

scope.matched = true;
const std::size_t backendLength =
path.size() - prefix.size() - suffix.size();
const std::string backendId =
path.substr(prefix.size(), backendLength);
if (validBackendId(backendId))
{
scope.backendId = backendId;
}
return scope;
}

inline HttpServerRequest forAuthorization(const HttpServerRequest& request)
{
if (request.method != "POST")
{
return request;
}

const RouteScope scope = routeScope(request.path);
if (!scope.matched)
{
return request;
}

HttpServerRequest scoped = request;
const std::string injected =
"\"backendId\":\"" + scope.backendId + "\"";
const std::size_t objectStart =
scoped.body.find_first_not_of(" \t\r\n");
if (objectStart == std::string::npos || scoped.body[objectStart] != '{')
{
scoped.body = "{" + injected + "}";
return scoped;
}

const std::size_t firstContent =
scoped.body.find_first_not_of(" \t\r\n", objectStart + 1U);
const bool emptyObject =
firstContent != std::string::npos && scoped.body[firstContent] == '}';
scoped.body.insert(
objectStart + 1U,
injected + (emptyObject ? std::string() : std::string(",")));
return scoped;
}
}
Loading
Loading