work on csp header

This commit is contained in:
Jake Potrebic 2020-10-06 22:04:46 -07:00
parent 67d0bbedb5
commit 6f0d4b29e6
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
11 changed files with 105 additions and 53 deletions

View File

@ -1,5 +1,6 @@
[#ftl]
[#-- @implicitly included --]
[#-- @ftlvariable name="nonce" type="java.lang.String" --]
[#-- @ftlvariable name="mapper" type="com.fasterxml.jackson.databind.ObjectMapper" --]
[#-- @ftlvariable name="_csrf" type="org.springframework.security.web.csrf.CsrfToken" --]
[#-- @ftlvariable name="cu" type="io.papermc.hangar.db.model.UsersTable" --]

View File

@ -9,7 +9,6 @@ import io.papermc.hangar.security.voters.ProjectPermissionVoter;
import io.papermc.hangar.security.voters.UserLockVoter;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.UserService;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -32,9 +31,6 @@ import java.util.List;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String CSP = "script-src 'self'{nonce}";
public String CSP_NONCE;
private final HangarAuthenticationProvider authProvider;
private final PermissionService permissionService;
private final UserService userService;
@ -48,20 +44,18 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
CSP_NONCE = RandomStringUtils.randomAlphanumeric(64);
// TODO CSP nonce
// http.headers().contentSecurityPolicy(CSP.replace("{nonce}", " 'nonce-" + CSP_NONCE + "'"));
http.csrf().ignoringAntMatchers(
"/api/v2/authenticate", "/api/v2/sessions/current", "/api/v2/keys", "/api/sync_sso"
);
http.addFilter(new HangarAuthenticationFilter());
http.exceptionHandling().authenticationEntryPoint(new HangarAuthenticationEntryPoint());
http.authorizeRequests().anyRequest().permitAll().accessDecisionManager(accessDecisionManager()); // we use method security
http
.csrf()
.ignoringAntMatchers(
"/api/v2/authenticate",
"/api/v2/sessions/current",
"/api/v2/keys",
"/api/sync_sso"
)
.and()
.addFilter(new HangarAuthenticationFilter())
.exceptionHandling().authenticationEntryPoint(new HangarAuthenticationEntryPoint())
.and().authorizeRequests().anyRequest().permitAll().accessDecisionManager(accessDecisionManager());
}
@Override
@ -75,6 +69,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
return super.authenticationManager();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(

View File

@ -14,13 +14,11 @@ import io.papermc.hangar.db.model.ProjectVersionsTable;
import io.papermc.hangar.db.model.ProjectsTable;
import io.papermc.hangar.db.model.UserProjectRolesTable;
import io.papermc.hangar.db.model.UsersTable;
import io.papermc.hangar.model.Category;
import io.papermc.hangar.model.Platform;
import io.papermc.hangar.model.Role;
import io.papermc.hangar.model.SsoSyncData;
import io.papermc.hangar.model.TagColor;
import io.papermc.hangar.model.Visibility;
import io.papermc.hangar.model.generated.ProjectSortingStrategy;
import io.papermc.hangar.model.viewhelpers.ProjectPage;
import io.papermc.hangar.model.viewhelpers.UserData;
import io.papermc.hangar.security.annotations.UserLock;
@ -104,21 +102,21 @@ public class Apiv1Controller extends HangarController {
this.projectsTable = projectsTable;
}
@GetMapping("/v1/projects")
public ResponseEntity<ArrayNode> listProjects(@RequestParam(defaultValue = "") List<Category> categories, @RequestParam(defaultValue = "4") int sort, @RequestParam(required = false) String q, @RequestParam(required = false) Long limit, @RequestParam(required = false) Long offset) {
int maxLoad = hangarConfig.projects.getInitLoad();
long realLimit = ApiUtil.limitOrDefault(limit, maxLoad);
long realOffset = ApiUtil.offsetOrZero(offset);
ProjectSortingStrategy strategy;
try {
strategy = ProjectSortingStrategy.VALUES[sort];
} catch (ArrayIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
List<ProjectsTable> sortedProjects = v1ApiService.getProjects(q, categories.stream().map(Category::getValue).collect(Collectors.toList()), strategy, realLimit, realOffset);
return ResponseEntity.ok(writeProjects(sortedProjects));
}
// @GetMapping("/v1/projects")
// public ResponseEntity<ArrayNode> listProjects(@RequestParam(defaultValue = "") List<Category> categories, @RequestParam(defaultValue = "4") int sort, @RequestParam(required = false) String q, @RequestParam(required = false) Long limit, @RequestParam(required = false) Long offset) {
// int maxLoad = hangarConfig.projects.getInitLoad();
// long realLimit = ApiUtil.limitOrDefault(limit, maxLoad);
// long realOffset = ApiUtil.offsetOrZero(offset);
// ProjectSortingStrategy strategy;
// try {
// strategy = ProjectSortingStrategy.VALUES[sort];
// } catch (ArrayIndexOutOfBoundsException e) {
// throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
// }
//
// List<ProjectsTable> sortedProjects = v1ApiService.getProjects(q, categories.stream().map(Category::getValue).collect(Collectors.toList()), strategy, realLimit, realOffset);
// return ResponseEntity.ok(writeProjects(sortedProjects));
// }
@GetMapping("/v1/projects/{author}/{slug}")
public ResponseEntity<ObjectNode> showProject(@PathVariable String author, @PathVariable String slug) {
@ -126,11 +124,11 @@ public class Apiv1Controller extends HangarController {
return ResponseEntity.ok((ObjectNode) writeProjects(List.of(project)).get(0));
}
@GetMapping("v1/projects/{id}")
public ResponseEntity<ObjectNode> showProject(@PathVariable long id) {
ProjectsTable project = projectService.getProjectsTable(id);
return ResponseEntity.ok((ObjectNode) writeProjects(List.of(project)).get(0));
}
// @GetMapping("/v1/projects/{id}")
// public ResponseEntity<ObjectNode> showProject(@PathVariable long id) {
// ProjectsTable project = projectService.getProjectsTable(id);
// return ResponseEntity.ok((ObjectNode) writeProjects(List.of(project)).get(0));
// }
@PreAuthorize("@authenticationService.authV1ApiRequest(T(io.papermc.hangar.model.Permission).EditApiKeys, T(io.papermc.hangar.controller.util.ApiScope).forProject(#author, #slug))")
@UserLock

View File

@ -15,10 +15,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class HangarController {
@ -32,11 +35,15 @@ public abstract class HangarController {
private TemplateHelper templateHelper;
@Autowired
private ObjectMapper mapper;
@Autowired
private HttpServletResponse response;
@Autowired
protected Supplier<Optional<UsersTable>> currentUser;
private static final Pattern NONCE_PATTERN = Pattern.compile("(?<=nonce-)[a-zA-Z0-9]+");
protected ModelAndView fillModel(ModelAndView mav) {
// helpers
BeansWrapperBuilder builder = new BeansWrapperBuilder(Configuration.VERSION_2_3_30);
@ -62,6 +69,16 @@ public abstract class HangarController {
}
mav.addObject("cu", currentUser.get().orElse(null));
mav.addObject("headerData", userService.getHeaderData());
if (response.containsHeader("Content-Security-Policy")) {
Matcher nonceMatcher = NONCE_PATTERN.matcher(response.getHeader("Content-Security-Policy"));
if (!nonceMatcher.find()) {
throw new IllegalStateException("Must have script nonce defined");
}
String nonce = nonceMatcher.group();
mav.addObject("nonce", nonce);
} else {
mav.addObject("nonce", "missing-csp-header");
}
return mav;
}

View File

@ -0,0 +1,43 @@
package io.papermc.hangar.controller.filters;
import io.papermc.hangar.config.hangar.HangarConfig;
import org.apache.commons.lang3.RandomStringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ContentSecurityPolicyFilter extends OncePerRequestFilter {
public final HangarConfig hangarConfig;
private static final String CSP = "default-src 'self' {additional-uris} fonts.googleapis.com; style-src fonts.googleapis.com 'self' {additional-uris} 'unsafe-inline'; font-src fonts.gstatic.com; script-src {additional-uris} 'self' 'nonce-{nonce}' 'unsafe-eval'; img-src 'self' papermc.io paper.readthedocs.io {additional-uris} {auth-uri}; manifest-src {manifest-uri}; prefetch-src {prefetch-uri}; media {prefetch-uri}; object-src 'none'; block-all-mixed-content; frame-ancestors 'none'; base-uri 'none'";
@Autowired
public ContentSecurityPolicyFilter(HangarConfig hangarConfig) {
this.hangarConfig = hangarConfig;
}
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String CSP_NONCE = RandomStringUtils.randomAlphanumeric(64);
// response.addHeader(
// "Content-Security-Policy",
// CSP
// .replace("{additional-uris}", hangarConfig.isUseWebpack() ? "http://localhost:8081" : "")
// .replace("{auth-uri}", hangarConfig.getAuthUrl())
// .replace("{manifest-uri}", hangarConfig.isUseWebpack() ? "http://localhost:8081/manifest/manifest.json" : "'self'")
// .replace("{prefetch-uri}", hangarConfig.isUseWebpack() ? "http://localhost:8081" : "'self'")
// .replace("{nonce}", CSP_NONCE));
// TODO still some changes to make to the header
filterChain.doFilter(request, response);
}
}

View File

@ -1,8 +1,8 @@
package io.papermc.hangar.db.dao;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.mappers.DependencyMapper;
import io.papermc.hangar.db.mappers.PlatformDependencyMapper;
import io.papermc.hangar.db.mappers.VersionDependenciesMapper;
import io.papermc.hangar.db.model.ProjectVersionTagsTable;
import io.papermc.hangar.db.model.ProjectVersionsTable;
import io.papermc.hangar.model.generated.ReviewState;
@ -21,7 +21,7 @@ import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RegisterColumnMapper(DependencyMapper.class)
@RegisterColumnMapper(VersionDependenciesMapper.class)
@RegisterColumnMapper(PlatformDependencyMapper.class)
@RegisterBeanMapper(ProjectVersionsTable.class)
@RegisterBeanMapper(ProjectVersionTagsTable.class)

View File

@ -1,7 +1,7 @@
package io.papermc.hangar.db.dao.api;
import io.papermc.hangar.db.mappers.DependencyMapper;
import io.papermc.hangar.db.mappers.PlatformDependencyMapper;
import io.papermc.hangar.db.mappers.VersionDependenciesMapper;
import io.papermc.hangar.db.model.ProjectChannelsTable;
import io.papermc.hangar.db.model.ProjectVersionTagsTable;
import io.papermc.hangar.db.model.ProjectVersionsTable;
@ -54,7 +54,7 @@ public interface V1ApiDao {
@UseStringTemplateEngine
@RegisterBeanMapper(ProjectVersionsTable.class)
@RegisterColumnMapper(DependencyMapper.class)
@RegisterColumnMapper(VersionDependenciesMapper.class)
@RegisterColumnMapper(PlatformDependencyMapper.class)
@SqlQuery("SELECT pv.* " +
" FROM project_versions pv" +
@ -83,7 +83,7 @@ public interface V1ApiDao {
@KeyColumn("p_id")
@RegisterBeanMapper(ProjectVersionsTable.class)
@RegisterColumnMapper(DependencyMapper.class)
@RegisterColumnMapper(VersionDependenciesMapper.class)
@RegisterColumnMapper(PlatformDependencyMapper.class)
@SqlQuery("SELECT p.id p_id, pv.* FROM project_versions pv JOIN projects p ON pv.project_id = p.id WHERE p.recommended_version_id = pv.id AND p.id IN (<projectIds>)")
Map<Long, ProjectVersionsTable> getProjectsRecommendedVersion(@BindList(onEmpty = EmptyHandling.NULL_STRING) List<Long> projectIds);

View File

@ -1,7 +1,7 @@
package io.papermc.hangar.db.dao.api;
import io.papermc.hangar.db.dao.api.mappers.VersionMapper;
import io.papermc.hangar.db.mappers.DependencyMapper;
import io.papermc.hangar.db.mappers.VersionDependenciesMapper;
import io.papermc.hangar.model.generated.Version;
import io.papermc.hangar.model.generated.VersionStatsDay;
import org.jdbi.v3.sqlobject.config.KeyColumn;
@ -24,7 +24,7 @@ import java.util.Map;
public interface VersionsApiDao {
@UseStringTemplateEngine
@RegisterColumnMapper(DependencyMapper.class)
@RegisterColumnMapper(VersionDependenciesMapper.class)
@SqlQuery("SELECT pv.created_at," +
"pv.version_string," +
"pv.dependencies," +
@ -53,7 +53,7 @@ public interface VersionsApiDao {
"ORDER BY pv.created_at DESC LIMIT 1")
Version getVersion(String author, String slug, String versionString, @Define boolean canSeeHidden, @Define Long userId);
@RegisterColumnMapper(DependencyMapper.class)
@RegisterColumnMapper(VersionDependenciesMapper.class)
@UseStringTemplateEngine
@SqlQuery("SELECT pv.created_at," +
"pv.version_string," +

View File

@ -17,7 +17,7 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class DependencyMapper implements ColumnMapper<VersionDependencies> {
public class VersionDependenciesMapper implements ColumnMapper<VersionDependencies> {
private final ObjectMapper mapper = new ObjectMapper();

View File

@ -92,7 +92,7 @@ showFooter: Boolean = true, noContainer: Boolean = false, additionalMeta: Html =
</#if>
<#if scriptsEnabled>
<script>
<script nonce="${nonce}">
window.ROUTES = ${mapper.valueToTree(Routes.getJsRoutes())};
window.ROUTES.parse = function (key, ...params) {
var route = window.ROUTES[key];
@ -103,7 +103,7 @@ showFooter: Boolean = true, noContainer: Boolean = false, additionalMeta: Html =
};
</script>
<#if _csrf?? && _csrf.token??>
<script>
<script nonce="${nonce}">
window.csrf = '${_csrf.token}';
window.csrfInfo = {
'parameterName': '${_csrf.parameterName}',

View File

@ -57,6 +57,4 @@
</div>
<#-- <@modalManage.modalManage />-->
</@base.base>