j
This commit is contained in:
@@ -42,6 +42,7 @@ public class SecurityConfig {
|
||||
// Public endpoints
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers("/api/files/images/**").permitAll() // Public image serving
|
||||
.requestMatchers("/api/config/**").permitAll() // Public configuration endpoints
|
||||
.requestMatchers("/actuator/health").permitAll()
|
||||
// All other API endpoints require authentication
|
||||
.requestMatchers("/api/**").authenticated()
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.storycove.controller;
|
||||
|
||||
import com.storycove.dto.HtmlSanitizationConfigDto;
|
||||
import com.storycove.service.HtmlSanitizationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/config")
|
||||
public class HtmlSanitizationController {
|
||||
|
||||
private final HtmlSanitizationService htmlSanitizationService;
|
||||
|
||||
@Autowired
|
||||
public HtmlSanitizationController(HtmlSanitizationService htmlSanitizationService) {
|
||||
this.htmlSanitizationService = htmlSanitizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML sanitization configuration for frontend use
|
||||
* This allows the frontend to use the same sanitization rules as the backend
|
||||
*/
|
||||
@GetMapping("/html-sanitization")
|
||||
public ResponseEntity<HtmlSanitizationConfigDto> getHtmlSanitizationConfig() {
|
||||
HtmlSanitizationConfigDto config = htmlSanitizationService.getConfiguration();
|
||||
return ResponseEntity.ok(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.storycove.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HtmlSanitizationConfigDto {
|
||||
private List<String> allowedTags;
|
||||
private Map<String, List<String>> allowedAttributes;
|
||||
private List<String> allowedCssProperties;
|
||||
private Map<String, List<String>> removedAttributes;
|
||||
private String description;
|
||||
|
||||
public HtmlSanitizationConfigDto() {}
|
||||
|
||||
public List<String> getAllowedTags() {
|
||||
return allowedTags;
|
||||
}
|
||||
|
||||
public void setAllowedTags(List<String> allowedTags) {
|
||||
this.allowedTags = allowedTags;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getAllowedAttributes() {
|
||||
return allowedAttributes;
|
||||
}
|
||||
|
||||
public void setAllowedAttributes(Map<String, List<String>> allowedAttributes) {
|
||||
this.allowedAttributes = allowedAttributes;
|
||||
}
|
||||
|
||||
public List<String> getAllowedCssProperties() {
|
||||
return allowedCssProperties;
|
||||
}
|
||||
|
||||
public void setAllowedCssProperties(List<String> allowedCssProperties) {
|
||||
this.allowedCssProperties = allowedCssProperties;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getRemovedAttributes() {
|
||||
return removedAttributes;
|
||||
}
|
||||
|
||||
public void setRemovedAttributes(Map<String, List<String>> removedAttributes) {
|
||||
this.removedAttributes = removedAttributes;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,113 @@
|
||||
package com.storycove.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.storycove.dto.HtmlSanitizationConfigDto;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class HtmlSanitizationService {
|
||||
|
||||
private final Safelist allowlist;
|
||||
private static final Logger logger = LoggerFactory.getLogger(HtmlSanitizationService.class);
|
||||
private final ObjectMapper objectMapper;
|
||||
private Safelist allowlist;
|
||||
private HtmlSanitizationConfigDto config;
|
||||
|
||||
public HtmlSanitizationService() {
|
||||
// Create a custom allowlist for story content
|
||||
this.allowlist = Safelist.relaxed()
|
||||
// Basic formatting
|
||||
.addTags("p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6")
|
||||
// Text formatting
|
||||
.addTags("b", "strong", "i", "em", "u", "s", "strike", "del", "ins")
|
||||
.addTags("sup", "sub", "small", "big", "mark", "pre", "code")
|
||||
// Lists
|
||||
.addTags("ul", "ol", "li", "dl", "dt", "dd")
|
||||
// Links (but remove href for security)
|
||||
.addTags("a").removeAttributes("a", "href", "target")
|
||||
// Tables
|
||||
.addTags("table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption")
|
||||
// Quotes
|
||||
.addTags("blockquote", "cite", "q")
|
||||
// Horizontal rule
|
||||
.addTags("hr")
|
||||
// Allow basic styling attributes
|
||||
.addAttributes("p", "class", "style")
|
||||
.addAttributes("div", "class", "style")
|
||||
.addAttributes("span", "class", "style")
|
||||
.addAttributes("h1", "class", "style")
|
||||
.addAttributes("h2", "class", "style")
|
||||
.addAttributes("h3", "class", "style")
|
||||
.addAttributes("h4", "class", "style")
|
||||
.addAttributes("h5", "class", "style")
|
||||
.addAttributes("h6", "class", "style")
|
||||
// Allow limited CSS properties
|
||||
.addProtocols("style", "color", "background-color", "font-size", "font-weight",
|
||||
"font-style", "text-align", "text-decoration", "margin", "padding");
|
||||
public HtmlSanitizationService(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initializeSanitization() {
|
||||
loadConfiguration();
|
||||
createSafelist();
|
||||
}
|
||||
|
||||
private void loadConfiguration() {
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource("html-sanitization-config.json");
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
this.config = objectMapper.readValue(inputStream, HtmlSanitizationConfigDto.class);
|
||||
logger.info("Loaded HTML sanitization configuration with {} allowed tags",
|
||||
config.getAllowedTags().size());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load HTML sanitization configuration, using fallback", e);
|
||||
createFallbackConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
private void createFallbackConfiguration() {
|
||||
// Fallback configuration if JSON loading fails
|
||||
this.config = new HtmlSanitizationConfigDto();
|
||||
this.config.setAllowedTags(List.of(
|
||||
"p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"b", "strong", "i", "em", "u", "s", "strike", "del", "ins",
|
||||
"sup", "sub", "small", "big", "mark", "pre", "code",
|
||||
"ul", "ol", "li", "dl", "dt", "dd", "a",
|
||||
"table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption",
|
||||
"blockquote", "cite", "q", "hr"
|
||||
));
|
||||
this.config.setAllowedCssProperties(List.of(
|
||||
"color", "background-color", "font-size", "font-weight",
|
||||
"font-style", "text-align", "text-decoration", "margin", "padding"
|
||||
));
|
||||
}
|
||||
|
||||
private void createSafelist() {
|
||||
this.allowlist = new Safelist();
|
||||
|
||||
// Add allowed tags
|
||||
if (config.getAllowedTags() != null) {
|
||||
config.getAllowedTags().forEach(allowlist::addTags);
|
||||
}
|
||||
|
||||
// Add allowed attributes
|
||||
if (config.getAllowedAttributes() != null) {
|
||||
for (Map.Entry<String, List<String>> entry : config.getAllowedAttributes().entrySet()) {
|
||||
String tag = entry.getKey();
|
||||
List<String> attributes = entry.getValue();
|
||||
if (attributes != null) {
|
||||
allowlist.addAttributes(tag, attributes.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove specific attributes (like href from links for security)
|
||||
if (config.getRemovedAttributes() != null) {
|
||||
for (Map.Entry<String, List<String>> entry : config.getRemovedAttributes().entrySet()) {
|
||||
String tag = entry.getKey();
|
||||
List<String> attributes = entry.getValue();
|
||||
if (attributes != null) {
|
||||
attributes.forEach(attr -> allowlist.removeAttributes(tag, attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add allowed CSS properties for style attributes
|
||||
if (config.getAllowedCssProperties() != null) {
|
||||
config.getAllowedCssProperties().forEach(property ->
|
||||
allowlist.addProtocols("style", property));
|
||||
}
|
||||
|
||||
logger.info("Created Jsoup Safelist with configuration");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current sanitization configuration for sharing with frontend
|
||||
*/
|
||||
public HtmlSanitizationConfigDto getConfiguration() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public String sanitize(String html) {
|
||||
|
||||
34
backend/src/main/resources/html-sanitization-config.json
Normal file
34
backend/src/main/resources/html-sanitization-config.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"allowedTags": [
|
||||
"p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"b", "strong", "i", "em", "u", "s", "strike", "del", "ins",
|
||||
"sup", "sub", "small", "big", "mark", "pre", "code",
|
||||
"ul", "ol", "li", "dl", "dt", "dd",
|
||||
"a", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption",
|
||||
"blockquote", "cite", "q", "hr"
|
||||
],
|
||||
"allowedAttributes": {
|
||||
"p": ["class", "style"],
|
||||
"div": ["class", "style"],
|
||||
"span": ["class", "style"],
|
||||
"h1": ["class", "style"],
|
||||
"h2": ["class", "style"],
|
||||
"h3": ["class", "style"],
|
||||
"h4": ["class", "style"],
|
||||
"h5": ["class", "style"],
|
||||
"h6": ["class", "style"],
|
||||
"a": ["class"],
|
||||
"table": ["class"],
|
||||
"td": ["class", "colspan", "rowspan"],
|
||||
"th": ["class", "colspan", "rowspan"]
|
||||
},
|
||||
"allowedCssProperties": [
|
||||
"color", "background-color", "font-size", "font-weight",
|
||||
"font-style", "text-align", "text-decoration", "margin",
|
||||
"padding", "text-indent", "line-height"
|
||||
],
|
||||
"removedAttributes": {
|
||||
"a": ["href", "target"]
|
||||
},
|
||||
"description": "HTML sanitization configuration for StoryCove story content. This configuration is shared between frontend (DOMPurify) and backend (Jsoup) to ensure consistency."
|
||||
}
|
||||
Reference in New Issue
Block a user