From 6efee9157e6fc9d0e65cddbe20ebe72daff8b3ba Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 16:43:03 +0200 Subject: [PATCH 01/17] Fix building the server --- .../Core/Services/CalcpadService.cs | 10 +--------- Calcpad.Server/Dockerfile | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/Calcpad.Server/Core/Services/CalcpadService.cs b/Calcpad.Server/Core/Services/CalcpadService.cs index a46a545e..2cb26d8b 100644 --- a/Calcpad.Server/Core/Services/CalcpadService.cs +++ b/Calcpad.Server/Core/Services/CalcpadService.cs @@ -51,15 +51,7 @@ public Task ConvertAsync(string calcpadContent, CalcpadSettings? setting } }; - // Configure auth settings for #fetch if provided - if (settings?.Auth != null && !string.IsNullOrEmpty(settings.Auth.Url) && !string.IsNullOrEmpty(settings.Auth.JWT)) - { - macroParser.AuthSettings = new Calcpad.Core.AuthSettings - { - Url = settings.Auth.Url, - JWT = settings.Auth.JWT - }; - } + // TODO: AuthSettings support not yet available in Calcpad.Core MacroParser string outputText; var hasMacroErrors = macroParser.Parse(calcpadContent, out outputText, null, 0, true); diff --git a/Calcpad.Server/Dockerfile b/Calcpad.Server/Dockerfile index 3ca8abb7..a2bb079a 100644 --- a/Calcpad.Server/Dockerfile +++ b/Calcpad.Server/Dockerfile @@ -1,15 +1,17 @@ # Use the official .NET runtime as base image -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base WORKDIR /app EXPOSE 8080 EXPOSE 8081 # Use the official .NET SDK for building -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Copy the entire CalcpadVM directory to access all dependencies -COPY . . +# Copy the full repo (build context must be repo root) +COPY Calcpad.Core/ Calcpad.Core/ +COPY Calcpad.OpenXml/ Calcpad.OpenXml/ +COPY Calcpad.Server/ Calcpad.Server/ # Build the Calcpad.Server Linux project WORKDIR /src/Calcpad.Server @@ -30,7 +32,7 @@ RUN apt-get update && \ apt-get install -y \ chromium \ fonts-liberation \ - libasound2 \ + libasound2t64 \ libatk-bridge2.0-0 \ libdrm2 \ libgtk-3-0 \ @@ -46,9 +48,10 @@ RUN apt-get update && \ # Copy the published application COPY --from=publish /app/publish . -# Copy necessary files from Calcpad.Cli for templates and resources -COPY --from=build /src/Calcpad.Cli/doc ./doc -COPY --from=build /src/Calcpad.Cli/Fonts ./Fonts +# Copy necessary files from Calcpad.Cli for templates and resources (if needed) +# Note: Calcpad.Cli is not part of the build context; add it to the COPY stage if these are required +# COPY --from=build /src/Calcpad.Cli/doc ./doc +# COPY --from=build /src/Calcpad.Cli/Fonts ./Fonts # Set environment variables ENV ASPNETCORE_URLS=http://+:8080 From 91d93a379a31a47f9a14a26070b2d3df12536e34 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:46:25 +0200 Subject: [PATCH 02/17] server: Block #include in public mode Add IsPublicMode static property to CalcpadApiService, controlled by CALCPAD_PUBLIC_MODE env var. In public mode, #include directives are blocked to prevent path traversal. In local mode, file reading is unrestricted (trust local user). Dockerfile sets the env var. --- .../Core/Services/CalcpadApiService.cs | 12 ++++++ .../Core/Services/CalcpadService.cs | 42 +++++++++++-------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Calcpad.Server/Core/Services/CalcpadApiService.cs b/Calcpad.Server/Core/Services/CalcpadApiService.cs index 8cb98532..a6129645 100644 --- a/Calcpad.Server/Core/Services/CalcpadApiService.cs +++ b/Calcpad.Server/Core/Services/CalcpadApiService.cs @@ -9,6 +9,15 @@ namespace Calcpad.Server.Services /// public static class CalcpadApiService { + /// + /// When true, strict security restrictions are applied (public Docker server). + /// When false (default), the server runs in permissive local mode (Windows tray app). + /// Controlled by the CALCPAD_PUBLIC_MODE environment variable. + /// + public static bool IsPublicMode { get; } = + string.Equals(Environment.GetEnvironmentVariable("CALCPAD_PUBLIC_MODE"), "true", StringComparison.OrdinalIgnoreCase) + || Environment.GetEnvironmentVariable("CALCPAD_PUBLIC_MODE") == "1"; + /// /// Configure the web application builder with all necessary services /// @@ -54,6 +63,9 @@ public static WebApplication ConfigureApp(WebApplication app) app.UseCors("AllowAll"); app.MapControllers(); + var mode = IsPublicMode ? "PUBLIC" : "LOCAL"; + Console.WriteLine($"Calcpad server starting in {mode} mode"); + return app; } diff --git a/Calcpad.Server/Core/Services/CalcpadService.cs b/Calcpad.Server/Core/Services/CalcpadService.cs index 2cb26d8b..0abf74b7 100644 --- a/Calcpad.Server/Core/Services/CalcpadService.cs +++ b/Calcpad.Server/Core/Services/CalcpadService.cs @@ -32,27 +32,33 @@ public Task ConvertAsync(string calcpadContent, CalcpadSettings? setting // 2. Parse macros and includes (following WPF pattern) var macroParser = new MacroParser(); - // Set up the Include function for #include directives - macroParser.Include = (fileName, fields) => + if (CalcpadApiService.IsPublicMode) { - try + // #include is disabled in public mode to prevent path traversal attacks. + macroParser.Include = (fileName, fields) => { - if (!File.Exists(fileName)) - return $"' File not found: {fileName}"; - - // Simply read and return the file content - // Note: fields parameter is required by Calcpad.Core but not used in Server - return File.ReadAllText(fileName); - } - catch (Exception ex) + FileLogger.LogWarning("Blocked #include directive in public mode", fileName); + return "' #include is not supported in public mode"; + }; + } + else + { + // Local mode: allow #include with default file-reading behavior + macroParser.Include = (fileName, fields) => { - FileLogger.LogError($"Error reading include file: {fileName}", ex); - return $"' Error reading file: {fileName} - {ex.Message}"; - } - }; - - // TODO: AuthSettings support not yet available in Calcpad.Core MacroParser - + try + { + if (!File.Exists(fileName)) + return $"' File not found: {fileName}"; + return File.ReadAllText(fileName); + } + catch (Exception ex) + { + FileLogger.LogError($"Error reading include file: {fileName}", ex); + return $"' Error reading file: {fileName} - {ex.Message}"; + } + }; + } string outputText; var hasMacroErrors = macroParser.Parse(calcpadContent, out outputText, null, 0, true); From 154a0f297eb943904ffad183fc2a54415a96cf72 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:47:01 +0200 Subject: [PATCH 03/17] server: XSS prevention Add SanitizeHtml to strip dangerous HTML tags (script, iframe, object, embed, form, etc.) and event handler attributes from Calcpad output. Add Content-Security-Policy meta tag to HTML wrapper. These protections apply in both public and local modes. --- .../Core/Services/CalcpadService.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Calcpad.Server/Core/Services/CalcpadService.cs b/Calcpad.Server/Core/Services/CalcpadService.cs index 0abf74b7..ecb6b23d 100644 --- a/Calcpad.Server/Core/Services/CalcpadService.cs +++ b/Calcpad.Server/Core/Services/CalcpadService.cs @@ -1,5 +1,6 @@ using Calcpad.Core; using Calcpad.Server.Controllers; +using System.Text.RegularExpressions; namespace Calcpad.Server.Services { @@ -472,14 +473,39 @@ private string GetFallbackTemplate() "; } + private static readonly string CspMetaTag = + ""; + private string WrapHtmlResult(string htmlContent, string theme = "light") { + // Sanitize dangerous HTML tags from Calcpad output before wrapping + htmlContent = SanitizeHtml(htmlContent); + // Use the comprehensive HTML template with theme support var themeClass = theme.ToLower() == "dark" ? " class=\"dark-theme\"" : ""; var templateWithTheme = _htmlTemplate.Replace("", $""); + // Inject Content-Security-Policy to block inline scripts and external resources + templateWithTheme = templateWithTheme.Replace("", $"\n {CspMetaTag}"); return templateWithTheme.Replace("{{CONTENT}}", htmlContent); } + private static readonly Regex DangerousTagsRegex = new( + @"<\s*/?\s*(script|iframe|object|embed|form|base|link|meta|applet|svg\s+[^>]*on\w+)[^>]*>", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex EventHandlerRegex = new( + @"\s+on\w+\s*=\s*[""'][^""']*[""']", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static string SanitizeHtml(string html) + { + // Strip dangerous tags: script, iframe, object, embed, form, base, link, meta, applet + html = DangerousTagsRegex.Replace(html, string.Empty); + // Strip event handler attributes (onclick, onerror, etc.) + html = EventHandlerRegex.Replace(html, string.Empty); + return html; + } + private static void TryDeleteFile(string filePath) { try From 2eda4cc3fe5d56644c85f4f2f5f493f7ccc09e1e Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:47:24 +0200 Subject: [PATCH 04/17] server: Computation timeout (conditional) Add wall-clock timeout for expression parsing. Public mode: 15 seconds. Local mode: 300 seconds (5 minutes) for long computations. Uses Task.Run with cancellation to enforce the limit. --- .../Core/Services/CalcpadService.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Calcpad.Server/Core/Services/CalcpadService.cs b/Calcpad.Server/Core/Services/CalcpadService.cs index ecb6b23d..575a780a 100644 --- a/Calcpad.Server/Core/Services/CalcpadService.cs +++ b/Calcpad.Server/Core/Services/CalcpadService.cs @@ -74,11 +74,27 @@ public Task ConvertAsync(string calcpadContent, CalcpadSettings? setting { try { - // 3. Parse expressions and calculate (following WPF pattern) + // 3. Parse expressions and calculate with a wall-clock timeout + var timeoutSeconds = CalcpadApiService.IsPublicMode ? 15 : 300; var parser = new ExpressionParser { Settings = coreSettings }; - parser.Parse(outputText, true, false); // calculate = true, getXml = false for HTML + var parseTask = Task.Run(() => + { + parser.Parse(outputText, true, false); + }); + if (!parseTask.Wait(TimeSpan.FromSeconds(timeoutSeconds))) + { + parser.Cancel(); + FileLogger.LogWarning($"Computation timeout after {timeoutSeconds} seconds"); + throw new TimeoutException($"Computation exceeded the maximum allowed time of {timeoutSeconds} seconds."); + } + // Re-throw any exception from the parse task + parseTask.GetAwaiter().GetResult(); htmlResult = parser.HtmlResult; } + catch (TimeoutException) + { + throw; + } catch (Exception parseEx) { FileLogger.LogWarning("Expression parsing failed, falling back to unwrapped code", parseEx.Message); From 5fe03581ad3bcfb51668efaedffa466650d0e828 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:47:47 +0200 Subject: [PATCH 05/17] server: SSRF prevention in PDF generation Inject Content-Security-Policy meta tag into HTML before Chromium renders it, blocking all external resource loading. Switch from Networkidle0 to DOMContentLoaded to avoid waiting for blocked fetches. Applies in both public and local modes. --- Calcpad.Server/Core/Services/PdfGeneratorService.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Calcpad.Server/Core/Services/PdfGeneratorService.cs b/Calcpad.Server/Core/Services/PdfGeneratorService.cs index 9450a4b9..2f6801ca 100644 --- a/Calcpad.Server/Core/Services/PdfGeneratorService.cs +++ b/Calcpad.Server/Core/Services/PdfGeneratorService.cs @@ -73,10 +73,14 @@ public async Task GeneratePdfAsync(string htmlContent, PdfSettings? pdfS using var page = await browser.NewPageAsync(); _logger.LogDebug("New page created successfully"); - // Set content and wait for it to load - await page.SetContentAsync(htmlContent, new NavigationOptions + // Inject Content-Security-Policy to block all external resource loading (SSRF prevention) + const string pdfCsp = ""; + var safeHtml = htmlContent.Replace("", $"\n{pdfCsp}"); + + // Set content and wait for DOM to be ready (not Networkidle0, which waits for external fetches) + await page.SetContentAsync(safeHtml, new NavigationOptions { - WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } + WaitUntil = new[] { WaitUntilNavigation.DOMContentLoaded } }); // Configure PDF options From 6cbe944020e4bc41610d31fb6d271b5749dd7b14 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:48:46 +0200 Subject: [PATCH 06/17] server: Rate limiting Add fixed-window rate limiting: convert=20/min, pdf=5/min, general=100/min in public mode. In local mode, limits are set to int.MaxValue (effectively unlimited). Add [EnableRateLimiting] attributes to controller endpoints. --- .../Core/Controllers/CalcpadController.cs | 4 +++ .../Core/Services/CalcpadApiService.cs | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Calcpad.Server/Core/Controllers/CalcpadController.cs b/Calcpad.Server/Core/Controllers/CalcpadController.cs index db033e7c..87ad114f 100644 --- a/Calcpad.Server/Core/Controllers/CalcpadController.cs +++ b/Calcpad.Server/Core/Controllers/CalcpadController.cs @@ -1,5 +1,6 @@ using Calcpad.Server.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; namespace Calcpad.Server.Controllers { @@ -17,6 +18,7 @@ public CalcpadController(CalcpadService calcpadService, PdfGeneratorService pdfG } [HttpPost("convert")] + [EnableRateLimiting("convert")] public async Task ConvertToHtml([FromBody] CalcpadRequest request) { try @@ -85,6 +87,7 @@ private async Task GeneratePdfResponse(string htmlContent, PdfSet } [HttpPost("convert-unwrapped")] + [EnableRateLimiting("convert")] public async Task ConvertToUnwrappedHtml([FromBody] CalcpadRequest request) { try @@ -104,6 +107,7 @@ public async Task ConvertToUnwrappedHtml([FromBody] CalcpadReques } [HttpGet("sample")] + [EnableRateLimiting("general")] public IActionResult GetSample() { var sampleContent = _calcpadService.GetSampleContent(); diff --git a/Calcpad.Server/Core/Services/CalcpadApiService.cs b/Calcpad.Server/Core/Services/CalcpadApiService.cs index a6129645..cd8b2566 100644 --- a/Calcpad.Server/Core/Services/CalcpadApiService.cs +++ b/Calcpad.Server/Core/Services/CalcpadApiService.cs @@ -1,5 +1,7 @@ using Calcpad.Server.Services; +using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; +using System.Threading.RateLimiting; namespace Calcpad.Server.Services { @@ -44,6 +46,34 @@ public static WebApplicationBuilder ConfigureBuilder(string[] args) }); }); + // Rate limiting — strict in public mode, effectively unlimited locally + builder.Services.AddRateLimiter(options => + { + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + var convertLimit = IsPublicMode ? 20 : int.MaxValue; + var pdfLimit = IsPublicMode ? 5 : int.MaxValue; + var generalLimit = IsPublicMode ? 100 : int.MaxValue; + + options.AddFixedWindowLimiter("convert", opt => + { + opt.PermitLimit = convertLimit; + opt.Window = TimeSpan.FromMinutes(1); + opt.QueueLimit = 0; + }); + options.AddFixedWindowLimiter("pdf", opt => + { + opt.PermitLimit = pdfLimit; + opt.Window = TimeSpan.FromMinutes(1); + opt.QueueLimit = 0; + }); + options.AddFixedWindowLimiter("general", opt => + { + opt.PermitLimit = generalLimit; + opt.Window = TimeSpan.FromMinutes(1); + opt.QueueLimit = 0; + }); + }); + return builder; } @@ -61,6 +91,7 @@ public static WebApplication ConfigureApp(WebApplication app) app.UseHttpsRedirection(); app.UseCors("AllowAll"); + app.UseRateLimiter(); app.MapControllers(); var mode = IsPublicMode ? "PUBLIC" : "LOCAL"; From fa3223992333fce761f1e9c5e52eb0c9cb43d55e Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:49:00 +0200 Subject: [PATCH 07/17] server: Request size limits Limit request body via Kestrel: 512 KB in public mode, 50 MB in local mode. The Kestrel-level limit is sufficient; no per-endpoint attributes needed. --- Calcpad.Server/Core/Services/CalcpadApiService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Calcpad.Server/Core/Services/CalcpadApiService.cs b/Calcpad.Server/Core/Services/CalcpadApiService.cs index cd8b2566..77ef4d5d 100644 --- a/Calcpad.Server/Core/Services/CalcpadApiService.cs +++ b/Calcpad.Server/Core/Services/CalcpadApiService.cs @@ -27,6 +27,12 @@ public static WebApplicationBuilder ConfigureBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); + // Limit request body size: 512 KB in public mode, 50 MB locally + builder.WebHost.ConfigureKestrel(options => + { + options.Limits.MaxRequestBodySize = IsPublicMode ? 512_000 : 50_000_000; + }); + // Add services to the container builder.Services.AddControllers() .AddApplicationPart(typeof(CalcpadApiService).Assembly); // Discover controllers from Core assembly From bd2cd96b2716b879c932e90cfabae0c2bdc0103c Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:49:38 +0200 Subject: [PATCH 08/17] server: Error message sanitization In public mode, return generic error messages to avoid leaking internal details. In local mode, return detailed error messages for debugging. Uses CalcpadApiService.IsPublicMode for the check. --- Calcpad.Server/Core/Controllers/CalcpadController.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Calcpad.Server/Core/Controllers/CalcpadController.cs b/Calcpad.Server/Core/Controllers/CalcpadController.cs index 87ad114f..8487c4e8 100644 --- a/Calcpad.Server/Core/Controllers/CalcpadController.cs +++ b/Calcpad.Server/Core/Controllers/CalcpadController.cs @@ -42,7 +42,9 @@ public async Task ConvertToHtml([FromBody] CalcpadRequest request } catch (Exception ex) { - return StatusCode(500, $"Error processing Calcpad content: {ex.Message}"); + return StatusCode(500, CalcpadApiService.IsPublicMode + ? "An internal error occurred while processing the request." + : $"Error processing Calcpad content: {ex.Message}"); } } @@ -56,7 +58,9 @@ private async Task GeneratePdfResponse(string htmlContent, PdfSet } catch (Exception ex) { - return StatusCode(500, $"PDF generation failed: {ex.Message}"); + return StatusCode(500, CalcpadApiService.IsPublicMode + ? "An internal error occurred during PDF generation." + : $"PDF generation failed: {ex.Message}"); } } @@ -102,7 +106,9 @@ public async Task ConvertToUnwrappedHtml([FromBody] CalcpadReques } catch (Exception ex) { - return StatusCode(500, $"Error processing Calcpad content: {ex.Message}"); + return StatusCode(500, CalcpadApiService.IsPublicMode + ? "An internal error occurred while processing the request." + : $"Error processing Calcpad content: {ex.Message}"); } } From 159c5e5c9064b02b0674cac5010e48b6ac6fe7f1 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:49:54 +0200 Subject: [PATCH 09/17] server: CORS restriction In public mode, restrict CORS to origins from CALCPAD_CORS_ORIGINS env var (default: https://calcpad-ce.org). In local mode, allow any origin so the browser-based client works from any address. --- .../Core/Services/CalcpadApiService.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Calcpad.Server/Core/Services/CalcpadApiService.cs b/Calcpad.Server/Core/Services/CalcpadApiService.cs index 77ef4d5d..bc7f2818 100644 --- a/Calcpad.Server/Core/Services/CalcpadApiService.cs +++ b/Calcpad.Server/Core/Services/CalcpadApiService.cs @@ -41,14 +41,25 @@ public static WebApplicationBuilder ConfigureBuilder(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); - // Add CORS policy + // Add CORS policy — restricted in public mode, open locally builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { - policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); + if (IsPublicMode) + { + var corsOrigins = Environment.GetEnvironmentVariable("CALCPAD_CORS_ORIGINS") + ?? "https://calcpad-ce.org"; + policy.WithOrigins(corsOrigins.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .AllowAnyMethod() + .AllowAnyHeader(); + } + else + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + } }); }); From 822751bac774c97963b4afe503fe70e978334cd9 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:50:11 +0200 Subject: [PATCH 10/17] server: Swagger restriction Only expose Swagger UI in local mode. In public mode, the API documentation is hidden to reduce the attack surface. --- Calcpad.Server/Core/Services/CalcpadApiService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Calcpad.Server/Core/Services/CalcpadApiService.cs b/Calcpad.Server/Core/Services/CalcpadApiService.cs index bc7f2818..1c6f014e 100644 --- a/Calcpad.Server/Core/Services/CalcpadApiService.cs +++ b/Calcpad.Server/Core/Services/CalcpadApiService.cs @@ -99,8 +99,8 @@ public static WebApplicationBuilder ConfigureBuilder(string[] args) /// public static WebApplication ConfigureApp(WebApplication app) { - // Configure the HTTP request pipeline - if (app.Environment.IsDevelopment()) + // Expose Swagger only in local mode (not on the public server) + if (!IsPublicMode) { app.UseSwagger(); app.UseSwaggerUI(); From 162fada4894b1e0462cbd2d4f78e183f2ad2ad78 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:50:36 +0200 Subject: [PATCH 11/17] server: Chromium hardening Remove --single-process flag (security risk: disables Chromium sandbox). Add --disable-remote-fonts, --disable-background-networking, --disable-sync, --disable-translate to minimize attack surface. Applies in both public and local modes. --- Calcpad.Server/Core/Services/PdfGeneratorService.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Calcpad.Server/Core/Services/PdfGeneratorService.cs b/Calcpad.Server/Core/Services/PdfGeneratorService.cs index 2f6801ca..fedb1687 100644 --- a/Calcpad.Server/Core/Services/PdfGeneratorService.cs +++ b/Calcpad.Server/Core/Services/PdfGeneratorService.cs @@ -254,16 +254,13 @@ private string[] GetBrowserArgs() "--hide-scrollbars", "--disable-crash-reporter", "--no-crash-upload", - "--disable-crashpad-for-testing" + "--disable-crashpad-for-testing", + "--disable-remote-fonts", + "--disable-background-networking", + "--disable-sync", + "--disable-translate" }; - // Minimal platform-specific args - only essential ones - if (isLinux || isWSL) - { - // Single process mode for better Docker compatibility - baseArgs.Add("--single-process"); - } - return baseArgs.Distinct().ToArray(); // Remove duplicates } From 7d979edbb02c6ec5c3dcf2158501238417c95852 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:50:50 +0200 Subject: [PATCH 12/17] server: Security headers Add response headers middleware: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Cache-Control: no-store, Referrer-Policy: no-referrer. Applies in both public and local modes. --- Calcpad.Server/Core/Services/CalcpadApiService.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Calcpad.Server/Core/Services/CalcpadApiService.cs b/Calcpad.Server/Core/Services/CalcpadApiService.cs index 1c6f014e..8475a5c5 100644 --- a/Calcpad.Server/Core/Services/CalcpadApiService.cs +++ b/Calcpad.Server/Core/Services/CalcpadApiService.cs @@ -107,6 +107,17 @@ public static WebApplication ConfigureApp(WebApplication app) } app.UseHttpsRedirection(); + + // Security headers + app.Use(async (context, next) => + { + context.Response.Headers["X-Content-Type-Options"] = "nosniff"; + context.Response.Headers["X-Frame-Options"] = "DENY"; + context.Response.Headers["Cache-Control"] = "no-store"; + context.Response.Headers["Referrer-Policy"] = "no-referrer"; + await next(); + }); + app.UseCors("AllowAll"); app.UseRateLimiter(); app.MapControllers(); From 03af6c11ae8524a8e1caf5942ca41ce2ad1c5313 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 17:51:19 +0200 Subject: [PATCH 13/17] server: Input validation Add ValidateSettings method to validate request parameters: decimals range, degrees values, scale factors, string lengths. Called in both convert endpoints. Applies in both public and local modes. --- .../Core/Controllers/CalcpadController.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Calcpad.Server/Core/Controllers/CalcpadController.cs b/Calcpad.Server/Core/Controllers/CalcpadController.cs index 8487c4e8..62968cca 100644 --- a/Calcpad.Server/Core/Controllers/CalcpadController.cs +++ b/Calcpad.Server/Core/Controllers/CalcpadController.cs @@ -28,6 +28,9 @@ public async Task ConvertToHtml([FromBody] CalcpadRequest request return BadRequest("Content is required"); } + var validationError = ValidateSettings(request); + if (validationError != null) return BadRequest(validationError); + // Get HTML result first var htmlResult = await _calcpadService.ConvertAsync(request.Content, request.Settings, request.ForceUnwrappedCode, request.Theme); @@ -101,6 +104,9 @@ public async Task ConvertToUnwrappedHtml([FromBody] CalcpadReques return BadRequest("Content is required"); } + var validationError = ValidateSettings(request); + if (validationError != null) return BadRequest(validationError); + var result = await _calcpadService.ConvertAsync(request.Content, request.Settings, forceUnwrappedCode: true, request.Theme); return Content(result, "text/html"); } @@ -119,6 +125,35 @@ public IActionResult GetSample() var sampleContent = _calcpadService.GetSampleContent(); return Ok(new CalcpadRequest { Content = sampleContent }); } + + private static string? ValidateSettings(CalcpadRequest request) + { + if (request.Settings?.Math != null) + { + var m = request.Settings.Math; + if (m.Decimals is < 0 or > 15) return "Decimals must be between 0 and 15."; + if (m.Degrees is not null and not (0 or 1)) return "Degrees must be 0 or 1."; + } + if (request.Settings?.Plot != null) + { + var p = request.Settings.Plot; + if (p.ScreenScaleFactor is < 0.1 or > 10) return "ScreenScaleFactor must be between 0.1 and 10."; + if (p.ImagePath?.Length > 200) return "ImagePath is too long."; + if (p.ImageUri?.Length > 500) return "ImageUri is too long."; + } + if (request.PdfSettings != null) + { + var pdf = request.PdfSettings; + if (pdf.Scale is < 0.1 or > 3.0) return "PDF scale must be between 0.1 and 3.0."; + if (pdf.DocumentTitle?.Length > 200) return "DocumentTitle is too long."; + if (pdf.DocumentSubtitle?.Length > 200) return "DocumentSubtitle is too long."; + if (pdf.Author?.Length > 100) return "Author is too long."; + if (pdf.Company?.Length > 100) return "Company is too long."; + if (pdf.Project?.Length > 100) return "Project is too long."; + } + if (request.Theme is not ("light" or "dark")) return "Theme must be 'light' or 'dark'."; + return null; + } } public class CalcpadRequest From d325179d02d35700f4a2eecf6499ab41b6d7eba7 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Sun, 29 Mar 2026 18:02:24 +0200 Subject: [PATCH 14/17] Add a GitHub action to build the docker image for the Calcpad server --- .github/workflows/docker.yml | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..96e53c62 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,46 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/calcpadce-server + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: Calcpad.Server/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From c4e6d2845da98d0cf60e23e35199a44e60c8d420 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Thu, 30 Apr 2026 20:10:02 +0200 Subject: [PATCH 15/17] server: Disable #read/#write/#append --- Calcpad.Server/Core/Services/CalcpadService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Calcpad.Server/Core/Services/CalcpadService.cs b/Calcpad.Server/Core/Services/CalcpadService.cs index 575a780a..1eb0b30c 100644 --- a/Calcpad.Server/Core/Services/CalcpadService.cs +++ b/Calcpad.Server/Core/Services/CalcpadService.cs @@ -36,6 +36,7 @@ public Task ConvertAsync(string calcpadContent, CalcpadSettings? setting if (CalcpadApiService.IsPublicMode) { // #include is disabled in public mode to prevent path traversal attacks. + calcpadContent = Regex.Replace(calcpadContent, @"(?m)^[ \t]*#(read|write|append)\b.*$", string.Empty); macroParser.Include = (fileName, fields) => { FileLogger.LogWarning("Blocked #include directive in public mode", fileName); From 86d25a6ff008f56374714a1c7f5a902de1a749bb Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Wed, 3 Jun 2026 07:04:07 +0200 Subject: [PATCH 16/17] Update .gitignore --- .gitignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.gitignore b/.gitignore index dfcfd56f..f6aa414d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +Calcpad.Web # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -93,6 +94,9 @@ StyleCopReport.xml *.svclog *.scc +# Files built by Visual Studio Code +*.csproj.lscache + # Chutzpah Test files _Chutzpah* @@ -275,6 +279,16 @@ FakesAssemblies/ .ntvs_analysis.dat node_modules/ +# MathJax bundle (downloaded during CI build) +docs/javascripts/mathjax/ + +# MkDocs build output +site/ + +# npm artifacts from local MathJax setup +package.json +package-lock.json + # Visual Studio 6 build log *.plg @@ -348,3 +362,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Installer output +Calcpad.Wpf/Installer/Output/ From aa8b9ec90441f5fae7576bfc2c657931bf250f1f Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Wed, 3 Jun 2026 07:51:26 +0200 Subject: [PATCH 17/17] Use the same CSS stylings as the WPF application --- Calcpad.Server/Core/template.html | 453 ++++++++++++------------------ Calcpad.Server/README.md | 2 +- 2 files changed, 188 insertions(+), 267 deletions(-) diff --git a/Calcpad.Server/Core/template.html b/Calcpad.Server/Core/template.html index 5446ec29..92f34ca1 100644 --- a/Calcpad.Server/Core/template.html +++ b/Calcpad.Server/Core/template.html @@ -1,15 +1,29 @@ - Calcpad + Created with CalcpadCE {{CONTENT}} - \ No newline at end of file + diff --git a/Calcpad.Server/README.md b/Calcpad.Server/README.md index 701437a4..b67b899e 100644 --- a/Calcpad.Server/README.md +++ b/Calcpad.Server/README.md @@ -88,7 +88,7 @@ The server will start as a console application with: ### Docker (Linux) **Basic Docker:** ```bash -docker build -t calcpad-server . +docker build -f Calcpad.Server/Dockerfile -t calcpad-server . docker run -p 9420:8080 calcpad-server ```