Add CSP to local web UI forbidding JavaScript eval()

This commit is contained in:
Max Berger
2026-03-18 21:04:50 +01:00
parent 238e3f9529
commit 99e9b7ec56
5 changed files with 34 additions and 14 deletions

View File

@@ -198,7 +198,10 @@ def _serve_traversable(
return NOT_FOUND return NOT_FOUND
content_type = MIMETYPES.get( content_type = MIMETYPES.get(
os.path.splitext(traversable.name)[1].lower(), FALLBACK_MIMETYPE) os.path.splitext(traversable.name)[1].lower(), FALLBACK_MIMETYPE)
headers = {"Content-Type": content_type} headers = {
"Content-Type": content_type,
"Content-Security-Policy": "default-src 'self'; object-src 'none'"
}
if isinstance(traversable, pathlib.Path): if isinstance(traversable, pathlib.Path):
headers["Last-Modified"] = time.strftime( headers["Last-Modified"] = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", "%a, %d %b %Y %H:%M:%S GMT",

View File

@@ -112,6 +112,7 @@ main {
#logoutview span { #logoutview span {
width: calc(100% - 60px); width: calc(100% - 60px);
display: inline-block; display: inline-block;
word-wrap: break-word;
} }
#logoutview a { #logoutview a {
@@ -191,6 +192,7 @@ main {
font-size: 1em; font-size: 1em;
max-height: 130px; max-height: 130px;
overflow: overlay; overflow: overlay;
word-wrap: break-word;
} }
#collectionsscene article:hover ul { #collectionsscene article:hover ul {
@@ -518,3 +520,7 @@ button.inline {
margin: 0 2px; margin: 0 2px;
width: 1.4em; width: 1.4em;
} }
.hidden {
display: none !important;
}

View File

@@ -14,17 +14,12 @@
<title>Radicale Web Interface</title> <title>Radicale Web Interface</title>
<link href="css/main.css" type="text/css" media="screen" rel="stylesheet"> <link href="css/main.css" type="text/css" media="screen" rel="stylesheet">
<link href="css/icon.png" type="image/png" rel="icon"> <link href="css/icon.png" type="image/png" rel="icon">
<style>
.hidden {
display: none !important;
}
</style>
<script type="module" src="js/main.js"></script> <script type="module" src="js/main.js"></script>
</head> </head>
<body> <body>
<nav id="logoutview" class="hidden"> <nav id="logoutview" class="hidden">
<span data-name="user" style="word-wrap:break-word;"></span> <span data-name="user"></span>
<a href="#" class="green" data-name="refresh" title="Refresh">Refresh</a> <a href="#" class="green" data-name="refresh" title="Refresh">Refresh</a>
<a href="#" class="red" data-name="logout" title="Logout">Logout</a> <a href="#" class="red" data-name="logout" title="Logout">Logout</a>
</nav> </nav>
@@ -86,8 +81,8 @@
<span data-name="WEBCAL">Webcal</span> <span data-name="WEBCAL">Webcal</span>
</small> </small>
<small data-name="contentcount"></small> <small data-name="contentcount"></small>
<input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);"> <input type="text" data-name="url" value="" readonly="" class="selectall">
<p data-name="description" style="word-wrap:break-word;">Description</p> <p data-name="description">Description</p>
<ul> <ul>
<li> <li>
<a href="" title="Download" class="green" data-name="download"> <a href="" title="Download" class="green" data-name="download">
@@ -162,7 +157,7 @@
</td> </td>
<td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td> <td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td>
<td><input type="text" data-name="pathortoken" value="" readonly="" <td><input type="text" data-name="pathortoken" value="" readonly=""
onfocus="this.setSelectionRange(0, 99999);" class="inline"></td> class="inline selectall"></td>
<td> <td>
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg" <button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
class="small_icon" alt="Delete"></button> class="small_icon" alt="Delete"></button>
@@ -187,7 +182,7 @@
</td> </td>
<td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td> <td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td>
<td><input type="text" data-name="pathortoken" value="" readonly="" <td><input type="text" data-name="pathortoken" value="" readonly=""
onfocus="this.setSelectionRange(0, 99999);" class="inline"></td> class="inline selectall"></td>
<td> <td>
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg" <button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
class="small_icon" alt="Delete"></button> class="small_icon" alt="Delete"></button>
@@ -225,7 +220,7 @@
<tbody data-name="incomingsharesbody"> <tbody data-name="incomingsharesbody">
<tr data-name="incomingsharerowtemplate" class="hidden"> <tr data-name="incomingsharerowtemplate" class="hidden">
<td><input type="text" data-name="pathortoken" value="" readonly="" <td><input type="text" data-name="pathortoken" value="" readonly=""
onfocus="this.setSelectionRange(0, 99999);" class="inline"></td> class="inline selectall"></td>
<td data-name="owner"></td> <td data-name="owner"></td>
<td data-name="permissions"><span class="pill" data-name="rw">rw</span><span class="pill" <td data-name="permissions"><span class="pill" data-name="rw">rw</span><span class="pill"
data-name="ro">ro</span></td> data-name="ro">ro</span></td>

View File

@@ -19,12 +19,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { LoginScene } from "./scenes/LoginScene.js";
import { LoadingScene } from "./scenes/LoadingScene.js"; import { LoadingScene } from "./scenes/LoadingScene.js";
import { LoginScene } from "./scenes/LoginScene.js";
import { push_scene } from "./scenes/scene_manager.js"; import { push_scene } from "./scenes/scene_manager.js";
import { setupSelectAll } from "./utils/misc.js";
// Add selection handler for input fields with 'selectall' class.
setupSelectAll();
// Hide startup loading message // Hide startup loading message
// This works because the LoadingScene is the one that is already active in index.html, // This works because the LoadingScene is the one that is already active in index.html,
// and all other scenes are hidden. // and all other scenes are hidden.
new LoadingScene().hide(); new LoadingScene().hide();
push_scene(new LoginScene()); push_scene(new LoginScene());

View File

@@ -108,3 +108,14 @@ export function bytesToHumanReadable(bytes) {
i = Math.min(i, units.length - 1); i = Math.min(i, units.length - 1);
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + units[i]; return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + units[i];
} }
/**
* Add selection handler for input fields with 'selectall' class.
*/
export function setupSelectAll() {
document.addEventListener("focusin", (event) => {
if (event.target instanceof HTMLInputElement && event.target.classList.contains("selectall")) {
event.target.setSelectionRange(0, 99999);
}
});
}