001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.http; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021/** 022 * Shared, dependency-free predicates for validating <b>same-origin</b> request paths declared by higher layers, plus 023 * the single {@link #MIN_POLL_INTERVAL_MS polling floor} those layers clamp declared poll intervals to. 024 * 025 * <p> 026 * This is the toolkit's one home for "is this app-declared endpoint safe to fetch from the browser" so the widgets 027 * and views modules can both <b>call</b> it without either depending on the other. The same-origin core 028 * ({@link #isSameOriginPath(String)}) rejects an absolute URL, a protocol-relative {@code //host} prefix, a 029 * scheme (colon-before-slash), and any {@code ..} path segment — the base rule every declarative endpoint 030 * shares. {@link #isNonTemplatedPath(String)} adds the stricter "no {@code {…}} placeholder" rule an endpoint that 031 * is <b>not</b> row/parameter-scoped requires (a card field-list, for example, is fixed — it is not fetched 032 * per-row, so a {@code {id}} template is meaningless and rejected). A path-templated caller (a calendar that 033 * legitimately needs {@code {year}}/{@code {month}}) layers {@link #isSafeTemplate(String, String...)} on top of 034 * {@link #isSameOriginPath(String)} rather than duplicating the same-origin core. Document links (an event 035 * {@code href}) use {@link #isSafeDocumentUrl(String)}, which is the same-origin core plus query/fragment. 036 * 037 * @since 10.0.0 038 */ 039public final class SafePathTemplate { 040 041 /** 042 * The minimum honored polling interval, in milliseconds. 043 * 044 * <p> 045 * A declared poll interval below this floor is clamped up to it rather than honored as configured — a 046 * declarable interval with no floor lets a consumer configure a self-inflicted load problem on a server-side 047 * endpoint. This is the single, easily-tested source of truth for the floor shared by every polling widget 048 * (view tables, card field-lists, calendars), so a stale/cached client script cannot be tricked into honoring a 049 * sub-floor value the server never actually declared. 050 */ 051 public static final long MIN_POLL_INTERVAL_MS = 5_000L; 052 053 private SafePathTemplate() {} 054 055 /** 056 * Whether {@code path} is a same-origin path: not blank, no {@code ://}, no {@code //} prefix, no scheme 057 * (colon before the first slash), and no {@code ..} path segments. 058 * 059 * <p> 060 * The check rejects, and never normalises and then accepts, the characters a browser's URL parser rewrites: a 061 * backslash (read as a slash, so {@code \\host} is protocol-relative), any C0 control character or DEL, and a 062 * first or last character at or below U+0020 (stripped, so {@code " //host"} resolves cross-host). Internal 063 * spaces and non-ASCII characters are still accepted. The JS guards in the console runtimes apply the same 064 * rule, and {@code same-origin-path-corpus.json} pins them to it. 065 * 066 * <p> 067 * This is the shared same-origin core. It deliberately says nothing about {@code {…}} template placeholders: 068 * a per-row/per-parameter caller (which needs {@code {id}}) uses this as-is, and a caller that forbids 069 * templating layers {@link #isNonTemplatedPath(String)} on top. 070 * 071 * @param path The candidate path. Can be <jk>null</jk>. 072 * @return <jk>true</jk> if the string is a same-origin path. 073 */ 074 @SuppressWarnings({ 075 "java:S3776" // Flat sequence of independent rejection checks; splitting it would obscure the security rules. 076 }) 077 public static boolean isSameOriginPath(String path) { 078 if (ib(path)) 079 return false; 080 if (path.charAt(0) <= ' ' || path.charAt(path.length() - 1) <= ' ') 081 return false; 082 for (var i = 0; i < path.length(); i++) { 083 var c = path.charAt(i); 084 if (c == '\\' || c < ' ' || c == 0x7f) 085 return false; 086 } 087 if (path.contains("://")) 088 return false; 089 if (path.startsWith("//")) 090 return false; 091 var colon = path.indexOf(':'); 092 var slash = path.indexOf('/'); 093 if (colon >= 0 && (slash < 0 || colon < slash)) 094 return false; 095 for (var seg : path.split("/", -1)) { 096 if (eq(seg, "..")) 097 return false; 098 } 099 return true; 100 } 101 102 /** 103 * Whether {@code path} is a same-origin path (per {@link #isSameOriginPath(String)}) that also carries <b>no</b> 104 * {@code {…}} template placeholder. 105 * 106 * <p> 107 * A fixed, non-parameterized endpoint (a card field-list refresh, for example) must be exactly the path it 108 * declares; a {@code {id}}-style placeholder has no substitution source and is rejected. 109 * 110 * @param path The candidate path. Can be <jk>null</jk>. 111 * @return <jk>true</jk> if the string is a same-origin, non-templated path. 112 */ 113 public static boolean isNonTemplatedPath(String path) { 114 return isSameOriginPath(path) && path.indexOf('{') < 0; 115 } 116 117 /** 118 * Clamps a declared poll interval up to {@link #MIN_POLL_INTERVAL_MS}. 119 * 120 * @param intervalMs The declared interval, in milliseconds. 121 * @return {@code max(intervalMs, }{@link #MIN_POLL_INTERVAL_MS}{@code )}. 122 */ 123 public static long clampPollInterval(long intervalMs) { 124 return Math.max(intervalMs, MIN_POLL_INTERVAL_MS); 125 } 126 127 /** 128 * Whether {@code template} is a same-origin path template (per {@link #isSameOriginPath(String)}) that also 129 * contains every required substitution token. 130 * 131 * <p> 132 * A per-month calendar GET like {@code /events/{year}/{month}} is same-origin <em>and</em> must actually 133 * declare the tokens the emitter will substitute. Built on {@link #isSameOriginPath(String)} rather than a 134 * second same-origin implementation. 135 * 136 * @param template The candidate template. Can be <jk>null</jk>. 137 * @param requiredTokens The literal tokens (for example {@code "{year}"}, {@code "{month}"}) that must all be 138 * present. May be empty (same-origin only). 139 * @return <jk>true</jk> if {@code template} is same-origin and contains every token. 140 */ 141 public static boolean isSafeTemplate(String template, String...requiredTokens) { 142 if (!isSameOriginPath(template)) 143 return false; 144 if (requiredTokens != null) 145 for (var t : requiredTokens) 146 if (t != null && !template.contains(t)) 147 return false; 148 return true; 149 } 150 151 /** 152 * Whether {@code url} is a same-origin <b>document</b> URL (query and fragment allowed). 153 * 154 * <p> 155 * Mirrors the client-side {@code isSafeDetailUrl} check. Query/hash never trip the absolute/scheme/{@code ..} 156 * rejections in {@link #isSameOriginPath(String)}; a {@code ?} value containing {@code ://} is still rejected. 157 * 158 * @param url The candidate document URL. Can be <jk>null</jk>. 159 * @return <jk>true</jk> if the URL is a safe same-origin document link. 160 */ 161 public static boolean isSafeDocumentUrl(String url) { 162 return isSameOriginPath(url); 163 } 164}