diff --git a/.gitattributes b/.gitattributes index 00800af..6aa2f92 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ * text=auto Tests/Assets/* binary +Tests/pre-build.sh binary \ No newline at end of file diff --git a/Assets/css/main.css b/Assets/css/main.css index 308f5d6..e765415 100644 --- a/Assets/css/main.css +++ b/Assets/css/main.css @@ -1,4 +1,4 @@ -/*! HTML5 Boilerplate v7.2.0 | MIT License | https://html5boilerplate.com/ */ +/*! HTML5 Boilerplate v7.3.0 | MIT License | https://html5boilerplate.com/ */ /* main.css 2.0.0 | MIT License | https://github.com/h5bp/main.css#readme */ /* diff --git a/Assets/js/vendor/modernizr-3.7.1.min.js b/Assets/js/vendor/modernizr-3.8.0.min.js similarity index 52% rename from Assets/js/vendor/modernizr-3.7.1.min.js rename to Assets/js/vendor/modernizr-3.8.0.min.js index 46c6ca2..dd27cab 100644 --- a/Assets/js/vendor/modernizr-3.7.1.min.js +++ b/Assets/js/vendor/modernizr-3.8.0.min.js @@ -1,3 +1,3 @@ -/*! modernizr 3.7.1 (Custom Build) | MIT * +/*! modernizr 3.8.0 (Custom Build) | MIT * * https://modernizr.com/download/?-cssanimations-csscolumns-customelements-flexbox-history-picture-pointerevents-postmessage-sizes-srcset-webgl-websockets-webworkers-addtest-domprefixes-hasevent-mq-prefixedcssvalue-prefixes-setclasses-testallprops-testprop-teststyles !*/ -!function(e,t,n){function r(e,t){return typeof e===t}function o(e){var t=b.className,n=Modernizr._config.classPrefix||"";if(S&&(t=t.baseVal),Modernizr._config.enableJSClass){var r=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(r,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(e.length>0&&(t+=" "+n+e.join(" "+n)),S?b.className.baseVal=t:b.className=t)}function i(e,t){if("object"==typeof e)for(var n in e)E(e,n)&&i(n,e[n]);else{e=e.toLowerCase();var r=e.split("."),s=Modernizr[r[0]];if(2===r.length&&(s=s[r[1]]),void 0!==s)return Modernizr;t="function"==typeof t?t():t,1===r.length?Modernizr[r[0]]=t:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=t),o([(t&&!1!==t?"":"no-")+r.join("-")]),Modernizr._trigger(e,t)}return Modernizr}function s(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):S?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}function a(){var e=t.body;return e||(e=s(S?"svg":"body"),e.fake=!0),e}function l(e,n,r,o){var i,l,u,f,c="modernizr",d=s("div"),p=a();if(parseInt(r,10))for(;r--;)u=s("div"),u.id=o?o[r]:c+(r+1),d.appendChild(u);return i=s("style"),i.type="text/css",i.id="s"+c,(p.fake?p:d).appendChild(i),p.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),d.id=c,p.fake&&(p.style.background="",p.style.overflow="hidden",f=b.style.overflow,b.style.overflow="hidden",b.appendChild(p)),l=n(d,e),p.fake?(p.parentNode.removeChild(p),b.style.overflow=f,b.offsetHeight):d.parentNode.removeChild(d),!!l}function u(e,t){return!!~(""+e).indexOf(t)}function f(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function c(t,n,r){var o;if("getComputedStyle"in e){o=getComputedStyle.call(e,t,n);var i=e.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(i){var s=i.error?"error":"log";i[s].call(i,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!n&&t.currentStyle&&t.currentStyle[r];return o}function d(t,r){var o=t.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(f(t[o]),r))return!0;return!1}if("CSSSupportsRule"in e){for(var i=[];o--;)i.push("("+f(t[o])+":"+r+")");return i=i.join(" or "),l("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"===c(e,null,"position")})}return n}function p(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function m(e,t,o,i){function a(){f&&(delete L.style,delete L.modElem)}if(i=!r(i,"undefined")&&i,!r(o,"undefined")){var l=d(e,o);if(!r(l,"undefined"))return l}for(var f,c,m,h,v,A=["modernizr","tspan","samp"];!L.style&&A.length;)f=!0,L.modElem=s(A.shift()),L.style=L.modElem.style;for(m=e.length,c=0;c0&&(t+=" "+n+e.join(" "+n)),S?b.className.baseVal=t:b.className=t)}function i(e,t){if("object"==typeof e)for(var n in e)P(e,n)&&i(n,e[n]);else{e=e.toLowerCase();var r=e.split("."),s=Modernizr[r[0]];if(2===r.length&&(s=s[r[1]]),void 0!==s)return Modernizr;t="function"==typeof t?t():t,1===r.length?Modernizr[r[0]]=t:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=t),o([(t&&!1!==t?"":"no-")+r.join("-")]),Modernizr._trigger(e,t)}return Modernizr}function s(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):S?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}function a(){var e=t.body;return e||(e=s(S?"svg":"body"),e.fake=!0),e}function l(e,n,r,o){var i,l,u,f,c="modernizr",d=s("div"),p=a();if(parseInt(r,10))for(;r--;)u=s("div"),u.id=o?o[r]:c+(r+1),d.appendChild(u);return i=s("style"),i.type="text/css",i.id="s"+c,(p.fake?p:d).appendChild(i),p.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),d.id=c,p.fake&&(p.style.background="",p.style.overflow="hidden",f=b.style.overflow,b.style.overflow="hidden",b.appendChild(p)),l=n(d,e),p.fake?(p.parentNode.removeChild(p),b.style.overflow=f,b.offsetHeight):d.parentNode.removeChild(d),!!l}function u(e,t){return!!~(""+e).indexOf(t)}function f(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function c(t,n,r){var o;if("getComputedStyle"in e){o=getComputedStyle.call(e,t,n);var i=e.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(i){var s=i.error?"error":"log";i[s].call(i,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!n&&t.currentStyle&&t.currentStyle[r];return o}function d(t,r){var o=t.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(f(t[o]),r))return!0;return!1}if("CSSSupportsRule"in e){for(var i=[];o--;)i.push("("+f(t[o])+":"+r+")");return i=i.join(" or "),l("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"===c(e,null,"position")})}return n}function p(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function m(e,t,o,i){function a(){f&&(delete L.style,delete L.modElem)}if(i=!r(i,"undefined")&&i,!r(o,"undefined")){var l=d(e,o);if(!r(l,"undefined"))return l}for(var f,c,m,h,A,v=["modernizr","tspan","samp"];!L.style&&v.length;)f=!0,L.modElem=s(v.shift()),L.style=L.modElem.style;for(m=e.length,c=0;c '') and IdemPChar(Pointer(ContentType), 'TEXT/HTML') then - AddCustomHeader(Context, 'Content-Security-Policy', - FContentSecurityPolicy); + AddCustomHeader(Context, 'Content-Security-Policy', + FContentSecurityPolicy); + + if (FContentSecurityPolicyReportOnly <> '') and + IdemPChar(Pointer(ContentType), 'TEXT/HTML') then + AddCustomHeader(Context, 'Content-Security-Policy-Report-Only', + FContentSecurityPolicyReportOnly); if FStrictSSL = strictSSLOn then if Context.UseSSL then @@ -1662,6 +1678,7 @@ function TBoilerplateHTTPServer.Request(Context: THttpServerRequest): Cardinal; if bpoDeleteXPoweredBy in LOptions then DeleteCustomHeader(Context, 'X-POWERED-BY:'); + Expires := 0; ExpiresDefined := False; if [bpoSetCacheNoTransform, bpoSetCachePublic, bpoSetCachePrivate, diff --git a/CSP.pas b/CSP.pas new file mode 100644 index 0000000..3638b6a --- /dev/null +++ b/CSP.pas @@ -0,0 +1,2117 @@ +/// Structures used for Content Security Policy Level 2 and Level 3 support +// Licensed under The MIT License (MIT) +unit CSP; + +(* + This unit is a path of integration project between HTML5 Boilerplate and + Synopse mORMot Framework. + + https://synopse.info + https://html5boilerplate.com + + Boilerplate HTTP Server + (c) 2016-Present Yevgeny Iliyn + + https://github.com/eugeneilyin/mORMotBP + + Version 2.2 + - First public release +*) + +interface + +{$I Synopse.inc} // define HASINLINE USETYPEINFO CPU32 CPU64 OWNNORMTOUPPER +{$IFDEF VER200}{$UNDEF HASINLINE}{$ENDIF} // Delphi 2009 has inlines issues + +uses + SynCommons, + SynCrypto, + SynCrtSock; + +type + + // Content security policy level 2 forward declarations + + PCSP2 = ^TCSP2; + PCSP2SourceList = ^TCSP2SourceList; + PCSP2FrameAncestors = ^TCSP2FrameAncestors; + PCSP2MediaTypeList = ^TCSP2MediaTypeList; + PCSP2URIReferences = ^TCSP2URIReferences; + PCSP2SandboxTokens = ^TCSP2SandboxTokens; + + /// Content security policy level 2 directives + TCSP2Directive = (csp2BaseURI, csp2ChildSrc, csp2ConnectSrc, csp2DefaultSrc, + csp2FontSrc, csp2FormAction, csp2FrameAncestors, csp2ImgSrc, csp2MediaSrc, + csp2ObjectSrc, csp2PluginTypes, csp2ReportURI, csp2Sandbox, csp2ScriptSrc, + csp2StyleSrc); + + /// Content security policy level 2 list of sources + TCSP2SourceList = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP2; + Directive: TCSP2Directive; + + /// Assign to the specific CSP directive + procedure Init(ACSP: PCSP2; const ADirective: TCSP2Directive); + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific source to the directive source list, + // used by other methods below + function Add(const Source: SockString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Empty source list and add signle "'none'" directive + // 'none' must not be mixed with other values + function None: PCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "null" host source + function Null: PCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "*" source (not recommended for production) + function Any: PCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme}:" source + // Section 7. Authors SHOULD NOT include either "'unsafe-inline'" or "data:" + // as valid sources in their policies. + function Scheme(const AScheme: SockString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme}://{host}:{port}{path}" source + function Host( + const AScheme, AHost, APort, APath: SockString): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{host}" source + function Host(const AHost: SockString): PCSP2SourceList; overload; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'self'" source + function WithSelf: PCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'unsafe-inline'" source + // Section 7. Authors SHOULD NOT include either 'unsafe-inline' or data: + // as valid sources in their policies. + function UnsafeInline: PCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'unsafe-eval'" source + function UnsafeEval: PCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'nonce-{value}'" source as a random generated sequence with + // specific length. + // Section 4.2. The generated value SHOULD be at least 128 bits long + // (before encoding), and generated via a cryptographically secure random + // number generator. + function NonceLen(out Base64EncodedNonce: SockString; + const NonceLength: Integer = 256): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'nonce-{value}'" source + // Section 4.2. The generated value SHOULD be at least 128 bits long + // (before encoding), and generated via a cryptographically secure random + // number generator. + function Nonce(const ANonce: RawByteString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'nonce-{value}'" where value is random generated + // Base64 encoded sequence + // Section 4.2. The generated value SHOULD be at least 128 bits long + // (before encoding), and generated via a cryptographically secure random + // number generator. + function Nonce64(const Base64EncodedNonce: SockString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" where value is a Base64 encodd hash calculated on + // the provided content + function SHA256(const Content: RawByteString; + const PBase64EncodedHash: PSockString = nil): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" with Base64 encoded hash + function SHA256Hash(const Hash: THash256): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" with Base64 encoded hash + function SHA256Hash(const Hash: RawByteString): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" with provided Base64 encoded hash + function SHA256Hash64(const Base64EncodedHash: SockString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" where value is a Base64 encodd hash calculated on + // the provided content + function SHA384(const Content: RawByteString; + const PBase64EncodedHash: PSockString = nil): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" with Base64 encoded hash + function SHA384Hash(const Hash: THash384): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" with Base64 encoded hash + function SHA384Hash(const Hash: RawByteString): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" with provided Base64 encoded hash + function SHA384Hash64(const Base64EncodedHash: SockString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" where value is a Base64 encodd hash calculated on + // the provided content + function SHA512(const Content: RawByteString; + const PBase64EncodedHash: PSockString = nil): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" with Base64 encoded hash + function SHA512Hash(const Hash: THash512): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" with Base64 encoded hash + function SHA512Hash(const Hash: RawByteString): PCSP2SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" with provided Base64 encoded hash + function SHA512Hash64(const Base64EncodedHash: SockString): PCSP2SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 2 frame ancestors + TCSP2FrameAncestors = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP2; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP2); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific source to the directive source list, + // used by other methods below + function Add(const Value: SockString): PCSP2FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'none'" source + function None: PCSP2FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme}:" source + // Section 7. Authors SHOULD NOT include either "'unsafe-inline'" or "data:" + // as valid sources in their policies. + function Scheme(const AScheme: SockString): PCSP2FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme}://{host}:{port}{path}" source + function Host(const AScheme, AHost, + APort, APath: SockString): PCSP2FrameAncestors; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{host}" source + function Host(const AHost: SockString): PCSP2FrameAncestors; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 2 media type list + TCSP2MediaTypeList = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP2; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP2); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific source to the directive source list, + // used by other methods below + function Add(const Value: SockString): PCSP2MediaTypeList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "/" source + function MediaType( + const AMediaType, AMediaSubtype: SockString): PCSP2MediaTypeList; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 2 URI references + TCSP2URIReferences = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP2; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP2); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific URI to the directive URI list, + // used by method below + function Add(const Value: SockString): PCSP2URIReferences; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add URI reference for reports sending + function Reference(const URIReference: SockString): PCSP2URIReferences; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 2 sandbox tokens + TCSP2SandboxTokens = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP2; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP2); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific token to the directive token list, + // used by other methods below + function Add(const Value: SockString = ''): PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Clear all sandbox tokens and add empty token + function Empty: PCSP2SandboxTokens; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add URI reference for reports sending + function Token(const AToken: SockString): PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-forms' flag + function AllowForms: PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-pointer-lock' flag + function AllowPointerLock: PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-popups' flag + function AllowPopups: PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-same-origin' flag + function AllowSameOrigin: PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-scripts' flag + function AllowScripts: PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-top-navigtion' flag + function AllowTopNavigation: PCSP2SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content Security Policy level 2 + TCSP2 = {$IFDEF FPC_OR_UNICODE}record private{$ELSE}object protected{$ENDIF} + private + Directives: array[TCSP2Directive] of TSockStringDynArray; + Counts: array[TCSP2Directive] of PtrInt; + Cached: Boolean; + Cache: SockString; + public + /// Initialize structure + function Init: PCSP2; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'base-uri' directive + function BaseURI: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'child-src' directive + function ChildSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'connect-src' directive + function ConnectSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'default-src' directive + function DefaultSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'font-src' directive + function FontSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'form-action' directive + function FormAction: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'frame-ancestors' directive + function FrameAncestors: TCSP2FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'img-src' directive + function ImgSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'media-src' directive + function MediaSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'object-src' directive + function ObjectSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'plugin-types' directive + function PluginTypes: TCSP2MediaTypeList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'report-uri' directive + function ReportURI: TCSP2URIReferences; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'sandbox' directive + function Sandbox: TCSP2SandboxTokens; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'script-src' directive + function ScriptSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'style-src' directive + function StyleSrc: TCSP2SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Computes and cache security policy content + function Policy: SockString; + + /// Computes 'Content-Security-Policy' HTTP header + function HTTPHeader: SockString; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Computes 'Content-Security-Policy-Report-Only' HTTP header + function HTTPHeaderReportOnly: SockString; {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + // Content security policy level 3 forward declarations + + PCSP3 = ^TCSP3; + PCSP3SourceList = ^TCSP3SourceList; + PCSP3MediaTypeList = ^TCSP3MediaTypeList; + PCSP3SandboxTokens = ^TCSP3SandboxTokens; + PCSP3FrameAncestors = ^TCSP3FrameAncestors; + + /// Content security policy level 3 directives + TCSP3Directive = (csp3ChildSrc, csp3ConnectSrc, csp3DefaultSrc, csp3FontSrc, + csp3FrameSrc, csp3ImgSrc, csp3ManifestSrc, csp3MediaSrc, csp3PrefetchSrc, + csp3ObjectSrc, csp3ScriptSrc, csp3ScriptSrcElem, csp3ScriptSrcAttr, + csp3StyleSrc, csp3StyleSrcElem, csp3StyleSrcAttr, csp3WorkerSrc, + csp3BaseURI, csp3PluginTypes, csp3Sandbox, csp3FormAction, + csp3FrameAncestors, csp3NavigateTo, csp3ReportTo); + + /// Stable Content security policy level 3 extensions + // https://www.w3.org/TR/CSP3/#directives-elsewhere + TCSP3Extensions = set of (csp3BlockAllMixedContent, + csp3UpgradeInsecureRequests, csp3RequireSRIFor); + + /// Content security policy level 3 SRI requires + TCSP3SRIRequire = (csp3SRIScript, csp3SRIStyle, csp3SRIScriptStyle); + + /// Content security policy level 3 source List + TCSP3SourceList = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP3; + Directive: TCSP3Directive; + + /// Assign to the specific CSP directive + procedure Init(ACSP: PCSP3; const ADirective: TCSP3Directive); + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific source to the directive source list, + // used by other methods below + function Add(const Source: SockString): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Empty source list and add signle "'none'" directive + // 'none' must not be mixed with other values + function None: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "null" host source + function Null: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "*" host source (not recommended for production) + function Any: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme-part}:" source + // Section 6. In either case, developers SHOULD NOT include either + // 'unsafe-inline', or data: as valid sources in their policies. + // Both enable XSS attacks by allowing code to be included directly in the + // document itself; they are best avoided completely. + // - AScheme is defined in section 3.1 of RFC 3986. + function Scheme(const AScheme: SockString): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme-part}://{host-part}:{port-part}{path-part}" source + // - AScheme is defined in section 3.1 of RFC 3986. + // - AHost is "*" / [ "*." ] 1*host-char *( "." 1*host-char ) + // - APort is 1*DIGIT / "*" + // - APath is path-absolute from + // https://tools.ietf.org/html/rfc3986#section-3.3 + function Host( + const AScheme, AHost, APort, APath: SockString): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{host}" source + function Host(const AHost: SockString): PCSP3SourceList; overload; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'self'" keyword source + function WithSelf: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'unsafe-inline'" keyword source + // Section 6. In either case, developers SHOULD NOT include either + // 'unsafe-inline', or data: as valid sources in their policies. + // Both enable XSS attacks by allowing code to be included directly in the + // document itself; they are best avoided completely. + function UnsafeInline: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'unsafe-eval'" keyword source + function UnsafeEval: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'strict-dynamic'" keyword source + function StrictDynamic: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'unsafe-hashes'" keyword source + function UnsafeHashes: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'report-sample'" keyword source + function ReportSample: PCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'unsafe-allow-redirects'" keyword source + function UnsafeAllowRedirects: PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'nonce-{value}'" source as a random generated sequence with + // specific length. The server MUST generate a unique value each time + // it transmits a policy. The generated value SHOULD be at least 128 bits + // long (before encoding), and SHOULD be generated via a cryptographically + // secure random number generator in order to ensure that the value + // is difficult for an attacker to predict. + // - AsBase64url is used to specify base64url encoding, instead of base64 + function NonceLen(out Base64EncodedNonce: SockString; + const NonceLength: Integer = 256; + const AsBase64url: Boolean = False): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'nonce-{value}'" source + // The server MUST generate a unique value each time it transmits a policy. + // The generated value SHOULD be at least 128 bits long (before encoding), + // and SHOULD be generated via a cryptographically secure random number + // generator in order to ensure that the value is difficult for an attacker + // to predict. + // - AsBase64url is used to specify base64url encoding, instead of base64 + function Nonce(const ANonce: RawByteString; + const AsBase64url: Boolean = False): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'nonce-{value}'" where value is random generated + // The server MUST generate a unique value each time it transmits a policy. + // The generated value SHOULD be at least 128 bits long (before encoding), + // and SHOULD be generated via a cryptographically secure random number + // generator in order to ensure that the value is difficult for an attacker + // to predict. + // Base64 encoded sequence + function Nonce64(const Base64EncodedNonce: SockString): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" where value is a Base64 encodd hash calculated on + // the provided content + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA256(const Content: RawByteString; + const PBase64EncodedHash: PSockString = nil; + const AsBase64url: Boolean = False): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" with Base64 encoded hash + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA256Hash(const Hash: THash256; + const AsBase64url: Boolean = False): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" with Base64 encoded hash + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA256Hash(const Hash: RawByteString; + const AsBase64url: Boolean = False): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha256-{value}'" with provided Base64 encoded hash + function SHA256Hash64(const Base64EncodedHash: SockString): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" where value is a Base64 encodd hash calculated on + // the provided content + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA384(const Content: RawByteString; + const PBase64EncodedHash: PSockString = nil; + const AsBase64url: Boolean = False): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" with Base64 encoded hash + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA384Hash(const Hash: THash384; + const AsBase64url: Boolean = False): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" with Base64 encoded hash + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA384Hash(const Hash: RawByteString; + const AsBase64url: Boolean = False): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha384-{value}'" with provided Base64 encoded hash + function SHA384Hash64(const Base64EncodedHash: SockString): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" where value is a Base64 encodd hash calculated on + // the provided content + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA512(const Content: RawByteString; + const PBase64EncodedHash: PSockString = nil; + const AsBase64url: Boolean = False): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" with Base64 encoded hash + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA512Hash(const Hash: THash512; + const AsBase64url: Boolean = False): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" with Base64 encoded hash + // - AsBase64url is used to specify base64url encoding, instead of base64 + function SHA512Hash(const Hash: RawByteString; + const AsBase64url: Boolean = False): PCSP3SourceList; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'sha512-{value}'" with provided Base64 encoded hash + function SHA512Hash64(const Base64EncodedHash: SockString): PCSP3SourceList; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 3 media type list + TCSP3MediaTypeList = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP3; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP3); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific source to the directive source list, + // used by other methods below + function Add(const Value: SockString): PCSP3MediaTypeList; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "/" source + function MediaType( + const AMediaType, AMediaSubtype: SockString): PCSP3MediaTypeList; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 3 sandbox tokens + TCSP3SandboxTokens = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP3; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP3); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific token to the directive token list, + // used by other methods below + function Add(const Value: SockString = ''): PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Clear all sandbox tokens and add empty token + function Empty: PCSP3SandboxTokens; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add URI reference for reports sending + function Token(const AToken: SockString): PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-popups' flag + function AllowPopups: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-top-navigtion' flag + function AllowTopNavigation: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-top-navigation-by-user-activation' flag + function AllowTopNavigationByUserActivation: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-same-origin' flag + function AllowSameOrigin: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-forms' flag + function AllowForms: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-pointer-lock' flag + function AllowPointerLock: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-scripts' flag + function AllowScripts: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-popups-to-escape-sandbox' flag + function AllowPopupsToEscapeSandbox: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-modals' flag + function AllowModals: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-orientation-lock' flag + function AllowOrientationLock: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'allow-presentation' flag + function AllowPresentation: PCSP3SandboxTokens; + {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content security policy level 3 frame ancestors + TCSP3FrameAncestors = {$IFDEF FPC_OR_UNICODE}record{$ELSE}object{$ENDIF} + public + CSP: PCSP3; + + /// Assign to the specific CSP + procedure Init(ACSP: PCSP3); {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add the specific source to the directive source list, + // used by other methods below + function Add(const Value: SockString): PCSP3FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'none'" source + function None: PCSP3FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme}:" source + // Section 6. In either case, developers SHOULD NOT include either + // 'unsafe-inline', or data: as valid sources in their policies. + // Both enable XSS attacks by allowing code to be included directly in the + // document itself; they are best avoided completely. + function Scheme(const AScheme: SockString): PCSP3FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{scheme}://{host}:{port}{path}" source + function Host(const AScheme, AHost, + APort, APath: SockString): PCSP3FrameAncestors; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "{host}" source + function Host(const AHost: SockString): PCSP3FrameAncestors; + overload; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add "'self'" source + function WithSelf: PCSP3FrameAncestors; {$IFDEF HASINLINE}inline;{$ENDIF} + end; + + /// Content Security Policy Level 3 + TCSP3 = {$IFDEF FPC_OR_UNICODE}record private{$ELSE}object protected{$ENDIF} + private + Directives: array[TCSP3Directive] of TSockStringDynArray; + Counts: array[TCSP3Directive] of PtrInt; + Cached: Boolean; + Cache: SockString; + Extensions: TCSP3Extensions; + SRIRequire: TCSP3SRIRequire; + public + /// Initialize structure + function Init: PCSP3; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'child-src' directive + function ChildSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'connect-src' directive + function ConnectSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'default-src' directive + function DefaultSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'font-src' directive + function FontSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'frame-src' directive + function FrameSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'img-src' directive + function ImgSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'manifest-src' directive + function ManifestSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'media-src' directive + function MediaSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'prefetch-src' directive + function PrefetchSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'object-src' directive + function ObjectSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'script-src' directive + function ScriptSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'script-src-elem' directive + function ScriptSrcElem: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'script-src-attr' directive + function ScriptSrcAttr: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'style-src' directive + function StyleSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'style-src-elem' directive + function StyleSrcElem: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'style-src-attr' directive + function StyleSrcAttr: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'worker-src' directive + function WorkerSrc: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'base-uri' directive + function BaseURI: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'plugin-types' directive + function PluginTypes: TCSP3MediaTypeList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'sandbox' directive + function Sandbox: TCSP3SandboxTokens; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'form-action' directive + function FormAction: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'frame-ancestors' directive + function FrameAncestors: TCSP3FrameAncestors; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'navigate-to' directive + function NavigateTo: TCSP3SourceList; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'report-to' directive + function ReportTo(const AToken: SockString): PCSP3; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'block-all-mixed-content' extension directive + // https://www.w3.org/TR/mixed-content/#block-all-mixed-content + function BlockAllMixedContent: PCSP3; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'upgrade-insecure-requests' extension directive + // https://www.w3.org/TR/upgrade-insecure-requests/#delivery + function UpgradeInsecureRequests: PCSP3; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Add 'require-sri-for' extension directive + // https://www.w3.org/TR/SRI/ + function RequireSRIFor(const ASRIRequire: TCSP3SRIRequire): PCSP3; + {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Computes and cache security policy content + function Policy: SockString; + + /// Computes 'Content-Security-Policy' HTTP header + function HTTPHeader: SockString; {$IFDEF HASINLINE}inline;{$ENDIF} + + /// Computes 'Content-Security-Policy-Report-Only' HTTP header + function HTTPHeaderReportOnly: SockString; {$IFDEF HASINLINE}inline;{$ENDIF} + end; + +implementation + +{ TCSP2SourceList } + +function TCSP2SourceList.Add(const Source: SockString): PCSP2SourceList; +begin + if Length(Source) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[Directive]) = Counts[Directive] then + SetLength(Directives[Directive], Counts[Directive] + 4); + Directives[Directive][Counts[Directive]] := Source; + Inc(Counts[Directive]); + end; + Result := @Self; +end; + +function TCSP2SourceList.Any: PCSP2SourceList; +begin + Result := Add('*'); +end; + +function TCSP2SourceList.Host( + const AScheme, AHost, APort, APath: SockString): PCSP2SourceList; +var + Value: SockString; +begin + Value := AHost; + if Length(AScheme) > 0 then + Value := AScheme + '://' + Value; + if Length(APort) > 0 then + Value := Value + ':' + APort; + if Length(APath) > 0 then + Value := Value + APath; + Result := Add(Value); +end; + +function TCSP2SourceList.Host(const AHost: SockString): PCSP2SourceList; +begin + Result := Add(AHost); +end; + +procedure TCSP2SourceList.Init(ACSP: PCSP2; const ADirective: TCSP2Directive); +begin + CSP := ACSP; + Directive := ADirective; +end; + +function TCSP2SourceList.NonceLen(out Base64EncodedNonce: SockString; + const NonceLength: Integer): PCSP2SourceList; +begin + Base64EncodedNonce := BinToBase64(TAESPRNG.Main.Fill(NonceLength shr 3)); + Result := Add(FormatUTF8('''nonce-%''', [Base64EncodedNonce])); +end; + +function TCSP2SourceList.Nonce(const ANonce: RawByteString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''nonce-%''', [BinToBase64(ANonce)])); +end; + +function TCSP2SourceList.Nonce64( + const Base64EncodedNonce: SockString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''nonce-%''', [Base64EncodedNonce])); +end; + +function TCSP2SourceList.None: PCSP2SourceList; +begin + with CSP^ do + if not ((Counts[Directive] = 1) and + (Directives[Directive][0] = '''none''')) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[Directive], 1); + Directives[Directive][0] := '''none'''; + Counts[Directive] := 1; + end; + Result := @Self; +end; + +function TCSP2SourceList.Null: PCSP2SourceList; +begin + Result := Add('null'); +end; + +function TCSP2SourceList.Scheme(const AScheme: SockString): PCSP2SourceList; +begin + Result := Add(AScheme + ':'); +end; + +function TCSP2SourceList.WithSelf: PCSP2SourceList; +begin + Result := Add('''self'''); +end; + +function TCSP2SourceList.SHA256(const Content: RawByteString; + const PBase64EncodedHash: PSockString): PCSP2SourceList; +var + LSHA256: TSHA256; + Hash: THash256; + EncodedHash: SockString; +begin + LSHA256.Full(Pointer(Content), Length(Content), Hash); + EncodedHash := BinToBase64(PAnsiChar(@Hash[0]), 32); + Result := Add(FormatUTF8('''sha256-%''', [EncodedHash])); + if PBase64EncodedHash <> nil then + PBase64EncodedHash^ := EncodedHash; +end; + +function TCSP2SourceList.SHA256Hash(const Hash: THash256): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha256-%''', + [BinToBase64(PAnsiChar(@Hash[0]), 32)])); +end; + +function TCSP2SourceList.SHA256Hash(const Hash: RawByteString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha256-%''', + [BinToBase64(PAnsiChar(@Hash[1]), 32)])); +end; + +function TCSP2SourceList.SHA256Hash64( + const Base64EncodedHash: SockString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha256-%''', [Base64EncodedHash])); +end; + +function TCSP2SourceList.SHA384(const Content: RawByteString; + const PBase64EncodedHash: PSockString): PCSP2SourceList; +var + LSHA384: TSHA384; + Hash: THash384; + EncodedHash: SockString; +begin + LSHA384.Full(Pointer(Content), Length(Content), Hash); + EncodedHash := BinToBase64(PAnsiChar(@Hash[0]), 48); + Result := Add(FormatUTF8('''sha384-%''', [EncodedHash])); + if PBase64EncodedHash <> nil then + PBase64EncodedHash^ := EncodedHash; +end; + +function TCSP2SourceList.SHA384Hash(const Hash: THash384): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha384-%''', + [BinToBase64(PAnsiChar(@Hash[0]), 48)])); +end; + +function TCSP2SourceList.SHA384Hash(const Hash: RawByteString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha384-%''', + [BinToBase64(PAnsiChar(@Hash[1]), 48)])); +end; + +function TCSP2SourceList.SHA384Hash64( + const Base64EncodedHash: SockString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha384-%''', [Base64EncodedHash])); +end; + +function TCSP2SourceList.SHA512(const Content: RawByteString; + const PBase64EncodedHash: PSockString): PCSP2SourceList; +var + LSHA512: TSHA512; + Hash: THash512; + EncodedHash: SockString; +begin + LSHA512.Full(Pointer(Content), Length(Content), Hash); + EncodedHash := BinToBase64(PAnsiChar(@Hash[0]), 64); + Result := Add(FormatUTF8('''sha512-%''', [EncodedHash])); + if PBase64EncodedHash <> nil then + PBase64EncodedHash^ := EncodedHash; +end; + +function TCSP2SourceList.SHA512Hash(const Hash: THash512): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha512-%''', + [BinToBase64(PAnsiChar(@Hash[0]), 64)])); +end; + +function TCSP2SourceList.SHA512Hash(const Hash: RawByteString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha512-%''', + [BinToBase64(PAnsiChar(@Hash[1]), 64)])); +end; + +function TCSP2SourceList.SHA512Hash64( + const Base64EncodedHash: SockString): PCSP2SourceList; +begin + Result := Add(FormatUTF8('''sha512-%''', [Base64EncodedHash])); +end; + +function TCSP2SourceList.UnsafeEval: PCSP2SourceList; +begin + Result := Add('''unsafe-eval'''); +end; + +function TCSP2SourceList.UnsafeInline: PCSP2SourceList; +begin + Result := Add('''unsafe-inline'''); +end; + +{ TCSP2FrameAncestors } + +function TCSP2FrameAncestors.Add(const Value: SockString): PCSP2FrameAncestors; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp2FrameAncestors]) = + Counts[csp2FrameAncestors] then + SetLength(Directives[csp2FrameAncestors], + Counts[csp2FrameAncestors] + 4); + Directives[csp2FrameAncestors][Counts[csp2FrameAncestors]] := Value; + Inc(Counts[csp2FrameAncestors]); + end; + Result := @Self; +end; + +function TCSP2FrameAncestors.Host(const AScheme, AHost, APort, + APath: SockString): PCSP2FrameAncestors; +var + Value: SockString; +begin + Value := AHost; + if Length(AScheme) > 0 then + Value := AScheme + '://' + Value; + if Length(APort) > 0 then + Value := Value + ':' + APort; + if Length(APath) > 0 then + Value := Value + APath; + Result := Add(Value); +end; + +function TCSP2FrameAncestors.Host(const AHost: SockString): PCSP2FrameAncestors; +begin + Result := Add(AHost); +end; + +procedure TCSP2FrameAncestors.Init(ACSP: PCSP2); +begin + CSP := ACSP; +end; + +function TCSP2FrameAncestors.None: PCSP2FrameAncestors; +begin + with CSP^ do + if not ((Counts[csp2FrameAncestors] = 1) and + (Directives[csp2FrameAncestors][0] = '''none''')) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[csp2FrameAncestors], 1); + Directives[csp2FrameAncestors][0] := '''none'''; + Counts[csp2FrameAncestors] := 1; + end; + Result := @Self; +end; + +function TCSP2FrameAncestors.Scheme( + const AScheme: SockString): PCSP2FrameAncestors; +begin + Result := Add(AScheme + ':'); +end; + +{ TCSP2MediaTypeList } + +function TCSP2MediaTypeList.Add(const Value: SockString): PCSP2MediaTypeList; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp2PluginTypes]) = Counts[csp2PluginTypes] then + SetLength(Directives[csp2PluginTypes], Counts[csp2PluginTypes] + 4); + Directives[csp2PluginTypes][Counts[csp2PluginTypes]] := Value; + Inc(Counts[csp2PluginTypes]); + end; + Result := @Self; +end; + +procedure TCSP2MediaTypeList.Init(ACSP: PCSP2); +begin + CSP := ACSP; +end; + +function TCSP2MediaTypeList.MediaType( + const AMediaType, AMediaSubtype: SockString): PCSP2MediaTypeList; +begin + Result := Add(FormatUTF8('%/%', [AMediaType, AMediaSubtype])); +end; + +{ TCSP2URIReferences } + +function TCSP2URIReferences.Add(const Value: SockString): PCSP2URIReferences; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp2ReportURI]) = Counts[csp2ReportURI] then + SetLength(Directives[csp2ReportURI], Counts[csp2ReportURI] + 4); + Directives[csp2ReportURI][Counts[csp2ReportURI]] := Value; + Inc(Counts[csp2ReportURI]); + end; + Result := @Self; +end; + +procedure TCSP2URIReferences.Init(ACSP: PCSP2); +begin + CSP := ACSP; +end; + +function TCSP2URIReferences.Reference( + const URIReference: SockString): PCSP2URIReferences; +begin + Result := Add(URIReference); +end; + +{ TCSPSandboxTokens } + +function TCSP2SandboxTokens.Add(const Value: SockString): PCSP2SandboxTokens; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp2Sandbox]) = Counts[csp2Sandbox] then + SetLength(Directives[csp2Sandbox], Counts[csp2Sandbox] + 4); + Directives[csp2Sandbox][Counts[csp2Sandbox]] := Value; + Inc(Counts[csp2Sandbox]); + end; + Result := @Self; +end; + +function TCSP2SandboxTokens.AllowForms: PCSP2SandboxTokens; +begin + Result := Add('allow-forms'); +end; + +function TCSP2SandboxTokens.AllowPointerLock: PCSP2SandboxTokens; +begin + Result := Add('allow-pointer-lock'); +end; + +function TCSP2SandboxTokens.AllowPopups: PCSP2SandboxTokens; +begin + Result := Add('allow-popups'); +end; + +function TCSP2SandboxTokens.AllowSameOrigin: PCSP2SandboxTokens; +begin + Result := Add('allow-same-origin'); +end; + +function TCSP2SandboxTokens.AllowScripts: PCSP2SandboxTokens; +begin + Result := Add('allow-scripts'); +end; + +function TCSP2SandboxTokens.AllowTopNavigation: PCSP2SandboxTokens; +begin + Result := Add('allow-top-navigation'); +end; + +function TCSP2SandboxTokens.Empty: PCSP2SandboxTokens; +begin + with CSP^ do + if not ((Counts[csp2Sandbox] = 1) and + (Directives[csp2Sandbox][0] = '')) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[csp2Sandbox], 1); + Directives[csp2Sandbox][0] := ''; + Counts[csp2Sandbox] := 1; + end; + Result := @Self; +end; + +procedure TCSP2SandboxTokens.Init(ACSP: PCSP2); +begin + CSP := ACSP; +end; + +function TCSP2SandboxTokens.Token(const AToken: SockString): PCSP2SandboxTokens; +begin + Result := Add(AToken); +end; + +{ TCSP2 } + +function TCSP2.BaseURI: TCSP2SourceList; +begin + Result.Init(@Self, csp2BaseURI); +end; + +function TCSP2.ChildSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2ChildSrc); +end; + +function TCSP2.ConnectSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2ConnectSrc); +end; + +function TCSP2.DefaultSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2DefaultSrc); +end; + +function TCSP2.FontSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2FontSrc); +end; + +function TCSP2.FormAction: TCSP2SourceList; +begin + Result.Init(@Self, csp2FormAction); +end; + +function TCSP2.FrameAncestors: TCSP2FrameAncestors; +begin + Result.Init(@Self); +end; + +function TCSP2.HTTPHeader: SockString; +begin + Result := FormatUTF8('Content-Security-Policy: %'#$D#$A, [Policy]); +end; + +function TCSP2.HTTPHeaderReportOnly: SockString; +begin + Result := FormatUTF8( + 'Content-Security-Policy-Report-Only: %'#$D#$A, [Policy]); +end; + +function TCSP2.ImgSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2ImgSrc); +end; + +function TCSP2.Init: PCSP2; +var + PValues: ^TSockStringDynArray; + Directive: TCSP2Directive; +begin + PValues := @Directives[Low(TCSP2Directive)]; + for Directive := Low(TCSP2Directive) to High(TCSP2Directive) do + begin + if Length(PValues^) > 0 then + SetLength(PValues^, 0); + Inc(PValues); + end; + FillcharFast(Counts, SizeOf(Counts), 0); + Cached := True; + Cache := ''; + Result := @Self; +end; + +function TCSP2.MediaSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2MediaSrc); +end; + +function TCSP2.ObjectSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2ObjectSrc); +end; + +function TCSP2.PluginTypes: TCSP2MediaTypeList; +begin + Result.Init(@Self); +end; + +function TCSP2.Policy: SockString; +const + DIRECTIVE_NAMES: array[TCSP2Directive] of SockString = ( + 'base-uri', 'child-src', 'connect-src', 'default-src', 'font-src', + 'form-action', 'frame-ancestors', 'img-src', 'media-src', 'object-src', + 'plugin-types', 'report-uri', 'sandbox', 'script-src', 'style-src'); +var + Index, MaxSize: PtrInt; + PCount: PPtrInt; + Directive: TCSP2Directive; + Writer: TSynTempWriter; + Value: SockString; +begin + if not Cached then + begin + MaxSize := 0; + PCount := @Counts[Low(TCSP2Directive)]; + for Directive := Low(TCSP2Directive) to High(TCSP2Directive) do + begin + if PCount^ > 0 then + begin + Inc(MaxSize, 15 + 1 + 2); // Length('frame-ancestors') + ' ' + '; ' + for Index := 0 to PCount^ - 1 do + Inc(MaxSize, Length(Directives[Directive][Index]) * 3 + 1); + end; + Inc(PCount); + end; + Writer.Init(MaxSize); + try + PCount := @Counts[Low(TCSP2Directive)]; + for Directive := Low(TCSP2Directive) to High(TCSP2Directive) do + begin + if PCount^ > 0 then + begin + Writer.wr(Pointer(DIRECTIVE_NAMES[Directive])^, + Length(DIRECTIVE_NAMES[Directive])); + Writer.wrb(Ord(' ')); + for Index := 0 to PCount^ - 1 do + if Length(Directives[Directive][Index]) > 0 then + begin + Value := StringReplaceAll(StringReplaceAll( + Directives[Directive][Index], ';', '%3B'), ',', '%2C'); + Writer.wr(Pointer(Value)^, Length(Value)); + Writer.wrb(Ord(' ')); + end; + Dec(Writer.pos); + Writer.wrw(Ord(';') + Ord(' ') shl 8); + end; + Inc(PCount); + end; + if Writer.Position > 0 then + Dec(Writer.pos, 2); + Cache := Writer.AsBinary; + Cached := True; + finally + Writer.Done; + end; + end; + Result := Cache; +end; + +function TCSP2.ReportURI: TCSP2URIReferences; +begin + Result.Init(@Self); +end; + +function TCSP2.Sandbox: TCSP2SandboxTokens; +begin + Result.Init(@Self) +end; + +function TCSP2.ScriptSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2ScriptSrc); +end; + +function TCSP2.StyleSrc: TCSP2SourceList; +begin + Result.Init(@Self, csp2StyleSrc); +end; + +{ TCSP3SourceList } + +function TCSP3SourceList.Add(const Source: SockString): PCSP3SourceList; +begin + if Length(Source) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[Directive]) = Counts[Directive] then + SetLength(Directives[Directive], Counts[Directive] + 4); + Directives[Directive][Counts[Directive]] := Source; + Inc(Counts[Directive]); + end; + Result := @Self; +end; + +function TCSP3SourceList.Any: PCSP3SourceList; +begin + Result := Add('*'); +end; + +function TCSP3SourceList.Host(const AScheme, AHost, APort, + APath: SockString): PCSP3SourceList; +var + Value: SockString; +begin + Value := AHost; + if Length(AScheme) > 0 then + Value := AScheme + '://' + Value; + if Length(APort) > 0 then + Value := Value + ':' + APort; + if Length(APath) > 0 then + Value := Value + APath; + Result := Add(Value); +end; + +function TCSP3SourceList.Host(const AHost: SockString): PCSP3SourceList; +begin + Result := Add(AHost); +end; + +procedure TCSP3SourceList.Init(ACSP: PCSP3; const ADirective: TCSP3Directive); +begin + CSP := ACSP; + Directive := ADirective; +end; + +function TCSP3SourceList.Nonce(const ANonce: RawByteString; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''nonce-%''', [BinToBase64uri(ANonce)])) + else + Result := Add(FormatUTF8('''nonce-%''', [BinToBase64(ANonce)])); +end; + +function TCSP3SourceList.NonceLen(out Base64EncodedNonce: SockString; + const NonceLength: Integer; const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Base64EncodedNonce := BinToBase64uri(TAESPRNG.Main.Fill(NonceLength shr 3)) + else + Base64EncodedNonce := BinToBase64(TAESPRNG.Main.Fill(NonceLength shr 3)); + Result := Add(FormatUTF8('''nonce-%''', [Base64EncodedNonce])); +end; + +function TCSP3SourceList.Nonce64( + const Base64EncodedNonce: SockString): PCSP3SourceList; +begin + Result := Add(FormatUTF8('''nonce-%''', [Base64EncodedNonce])); +end; + +function TCSP3SourceList.None: PCSP3SourceList; +begin + with CSP^ do + if not ((Counts[Directive] = 1) and + (Directives[Directive][0] = '''none''')) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[Directive], 1); + Directives[Directive][0] := '''none'''; + Counts[Directive] := 1; + end; + Result := @Self; +end; + +function TCSP3SourceList.Null: PCSP3SourceList; +begin + Result := Add('null'); +end; + +function TCSP3SourceList.ReportSample: PCSP3SourceList; +begin + Result := Add('''report-sample'''); +end; + +function TCSP3SourceList.Scheme(const AScheme: SockString): PCSP3SourceList; +begin + Result := Add(AScheme + ':'); +end; + +function TCSP3SourceList.SHA256(const Content: RawByteString; + const PBase64EncodedHash: PSockString; + const AsBase64url: Boolean): PCSP3SourceList; +var + LSHA256: TSHA256; + Hash: THash256; + EncodedHash: SockString; +begin + LSHA256.Full(Pointer(Content), Length(Content), Hash); + if AsBase64url then + EncodedHash := BinToBase64uri(PAnsiChar(@Hash[0]), 32) + else + EncodedHash := BinToBase64(PAnsiChar(@Hash[0]), 32); + Result := Add(FormatUTF8('''sha256-%''', [EncodedHash])); + if PBase64EncodedHash <> nil then + PBase64EncodedHash^ := EncodedHash; +end; + +function TCSP3SourceList.SHA256Hash(const Hash: THash256; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''sha256-%''', + [BinToBase64uri(PAnsiChar(@Hash[0]), 32)])) + else + Result := Add(FormatUTF8('''sha256-%''', + [BinToBase64(PAnsiChar(@Hash[0]), 32)])); +end; + +function TCSP3SourceList.SHA256Hash(const Hash: RawByteString; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''sha256-%''', + [BinToBase64uri(PAnsiChar(@Hash[1]), 32)])) + else + Result := Add(FormatUTF8('''sha256-%''', + [BinToBase64(PAnsiChar(@Hash[1]), 32)])); +end; + +function TCSP3SourceList.SHA256Hash64( + const Base64EncodedHash: SockString): PCSP3SourceList; +begin + Result := Add(FormatUTF8('''sha256-%''', [Base64EncodedHash])); +end; + +function TCSP3SourceList.SHA384(const Content: RawByteString; + const PBase64EncodedHash: PSockString; + const AsBase64url: Boolean): PCSP3SourceList; +var + LSHA384: TSHA384; + Hash: THash384; + EncodedHash: SockString; +begin + LSHA384.Full(Pointer(Content), Length(Content), Hash); + if AsBase64url then + EncodedHash := BinToBase64uri(PAnsiChar(@Hash[0]), 48) + else + EncodedHash := BinToBase64(PAnsiChar(@Hash[0]), 48); + Result := Add(FormatUTF8('''sha384-%''', [EncodedHash])); + if PBase64EncodedHash <> nil then + PBase64EncodedHash^ := EncodedHash; +end; + +function TCSP3SourceList.SHA384Hash(const Hash: THash384; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''sha384-%''', + [BinToBase64uri(PAnsiChar(@Hash[0]), 48)])) + else + Result := Add(FormatUTF8('''sha384-%''', + [BinToBase64(PAnsiChar(@Hash[0]), 48)])); +end; + +function TCSP3SourceList.SHA384Hash(const Hash: RawByteString; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''sha384-%''', + [BinToBase64uri(PAnsiChar(@Hash[1]), 48)])) + else + Result := Add(FormatUTF8('''sha384-%''', + [BinToBase64(PAnsiChar(@Hash[1]), 48)])); +end; + +function TCSP3SourceList.SHA384Hash64( + const Base64EncodedHash: SockString): PCSP3SourceList; +begin + Result := Add(FormatUTF8('''sha384-%''', [Base64EncodedHash])); +end; + +function TCSP3SourceList.SHA512(const Content: RawByteString; + const PBase64EncodedHash: PSockString; + const AsBase64url: Boolean): PCSP3SourceList; +var + LSHA512: TSHA512; + Hash: THash512; + EncodedHash: SockString; +begin + LSHA512.Full(Pointer(Content), Length(Content), Hash); + if AsBase64url then + EncodedHash := BinToBase64uri(PAnsiChar(@Hash[0]), 64) + else + EncodedHash := BinToBase64(PAnsiChar(@Hash[0]), 64); + Result := Add(FormatUTF8('''sha512-%''', [EncodedHash])); + if PBase64EncodedHash <> nil then + PBase64EncodedHash^ := EncodedHash; +end; + +function TCSP3SourceList.SHA512Hash(const Hash: THash512; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''sha512-%''', + [BinToBase64uri(PAnsiChar(@Hash[0]), 64)])) + else + Result := Add(FormatUTF8('''sha512-%''', + [BinToBase64(PAnsiChar(@Hash[0]), 64)])); +end; + +function TCSP3SourceList.SHA512Hash(const Hash: RawByteString; + const AsBase64url: Boolean): PCSP3SourceList; +begin + if AsBase64url then + Result := Add(FormatUTF8('''sha512-%''', + [BinToBase64uri(PAnsiChar(@Hash[1]), 64)])) + else + Result := Add(FormatUTF8('''sha512-%''', + [BinToBase64(PAnsiChar(@Hash[1]), 64)])); +end; + +function TCSP3SourceList.SHA512Hash64( + const Base64EncodedHash: SockString): PCSP3SourceList; +begin + Result := Add(FormatUTF8('''sha512-%''', [Base64EncodedHash])); +end; + +function TCSP3SourceList.StrictDynamic: PCSP3SourceList; +begin + Result := Add('''strict-dynamic'''); +end; + +function TCSP3SourceList.UnsafeAllowRedirects: PCSP3SourceList; +begin + Result := Add('''unsafe-allow-redirects'''); +end; + +function TCSP3SourceList.UnsafeEval: PCSP3SourceList; +begin + Result := Add('''unsafe-eval'''); +end; + +function TCSP3SourceList.UnsafeHashes: PCSP3SourceList; +begin + Result := Add('''unsafe-hashes'''); +end; + +function TCSP3SourceList.UnsafeInline: PCSP3SourceList; +begin + Result := Add('''unsafe-inline'''); +end; + +function TCSP3SourceList.WithSelf: PCSP3SourceList; +begin + Result := Add('''self'''); +end; + +{ TCSP3MediaTypeList } + +function TCSP3MediaTypeList.Add(const Value: SockString): PCSP3MediaTypeList; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp3PluginTypes]) = Counts[csp3PluginTypes] then + SetLength(Directives[csp3PluginTypes], Counts[csp3PluginTypes] + 4); + Directives[csp3PluginTypes][Counts[csp3PluginTypes]] := Value; + Inc(Counts[csp3PluginTypes]); + end; + Result := @Self; +end; + +procedure TCSP3MediaTypeList.Init(ACSP: PCSP3); +begin + CSP := ACSP; +end; + +function TCSP3MediaTypeList.MediaType(const AMediaType, + AMediaSubtype: SockString): PCSP3MediaTypeList; +begin + Result := Add(FormatUTF8('%/%', [AMediaType, AMediaSubtype])); +end; + +{ TCSP3SandboxTokens } + +function TCSP3SandboxTokens.Add(const Value: SockString): PCSP3SandboxTokens; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp3Sandbox]) = Counts[csp3Sandbox] then + SetLength(Directives[csp3Sandbox], Counts[csp3Sandbox] + 4); + Directives[csp3Sandbox][Counts[csp3Sandbox]] := Value; + Inc(Counts[csp3Sandbox]); + end; + Result := @Self; +end; + +function TCSP3SandboxTokens.AllowForms: PCSP3SandboxTokens; +begin + Result := Add('allow-forms'); +end; + +function TCSP3SandboxTokens.AllowModals: PCSP3SandboxTokens; +begin + Result := Add('allow-modals'); +end; + +function TCSP3SandboxTokens.AllowOrientationLock: PCSP3SandboxTokens; +begin + Result := Add('allow-orientation-lock'); +end; + +function TCSP3SandboxTokens.AllowPointerLock: PCSP3SandboxTokens; +begin + Result := Add('allow-pointer-lock'); +end; + +function TCSP3SandboxTokens.AllowPopups: PCSP3SandboxTokens; +begin + Result := Add('allow-popups'); +end; + +function TCSP3SandboxTokens.AllowPopupsToEscapeSandbox: PCSP3SandboxTokens; +begin + Result := Add('allow-popups-to-escape-sandbox'); +end; + +function TCSP3SandboxTokens.AllowPresentation: PCSP3SandboxTokens; +begin + Result := Add('allow-presentation'); +end; + +function TCSP3SandboxTokens.AllowSameOrigin: PCSP3SandboxTokens; +begin + Result := Add('allow-same-origin'); +end; + +function TCSP3SandboxTokens.AllowScripts: PCSP3SandboxTokens; +begin + Result := Add('allow-scripts'); +end; + +function TCSP3SandboxTokens.AllowTopNavigation: PCSP3SandboxTokens; +begin + Result := Add('allow-top-navigtion'); +end; + +function TCSP3SandboxTokens.AllowTopNavigationByUserActivation: PCSP3SandboxTokens; +begin + Result := Add('allow-top-navigation-by-user-activation'); +end; + +function TCSP3SandboxTokens.Empty: PCSP3SandboxTokens; +begin + with CSP^ do + if not ((Counts[csp3Sandbox] = 1) and + (Directives[csp3Sandbox][0] = '')) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[csp3Sandbox], 1); + Directives[csp3Sandbox][0] := ''; + Counts[csp3Sandbox] := 1; + end; + Result := @Self; +end; + +procedure TCSP3SandboxTokens.Init(ACSP: PCSP3); +begin + CSP := ACSP; +end; + +function TCSP3SandboxTokens.Token(const AToken: SockString): PCSP3SandboxTokens; +begin + Result := Add(AToken); +end; + +{ TCSP3FrameAncestors } + +function TCSP3FrameAncestors.Add(const Value: SockString): PCSP3FrameAncestors; +begin + if Length(Value) > 0 then + with CSP^ do + begin + Cache := ''; + Cached := False; + if Length(Directives[csp3FrameAncestors]) = + Counts[csp3FrameAncestors] then + SetLength(Directives[csp3FrameAncestors], + Counts[csp3FrameAncestors] + 4); + Directives[csp3FrameAncestors][Counts[csp3FrameAncestors]] := Value; + Inc(Counts[csp3FrameAncestors]); + end; + Result := @Self; +end; + +function TCSP3FrameAncestors.Host(const AScheme, AHost, APort, + APath: SockString): PCSP3FrameAncestors; +var + Value: SockString; +begin + Value := AHost; + if Length(AScheme) > 0 then + Value := AScheme + '://' + Value; + if Length(APort) > 0 then + Value := Value + ':' + APort; + if Length(APath) > 0 then + Value := Value + APath; + Result := Add(Value); +end; + +function TCSP3FrameAncestors.Host(const AHost: SockString): PCSP3FrameAncestors; +begin + Result := Add(AHost); +end; + +procedure TCSP3FrameAncestors.Init(ACSP: PCSP3); +begin + CSP := ACSP; +end; + +function TCSP3FrameAncestors.None: PCSP3FrameAncestors; +begin + with CSP^ do + if not ((Counts[csp3FrameAncestors] = 1) and + (Directives[csp3FrameAncestors][0] = '''none''')) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[csp3FrameAncestors], 1); + Directives[csp3FrameAncestors][0] := '''none'''; + Counts[csp3FrameAncestors] := 1; + end; + Result := @Self; +end; + +function TCSP3FrameAncestors.Scheme( + const AScheme: SockString): PCSP3FrameAncestors; +begin + Result := Add(AScheme + ':'); +end; + +function TCSP3FrameAncestors.WithSelf: PCSP3FrameAncestors; +begin + Result := Add('''self''') +end; + +{ TCSP3 } + +function TCSP3.BaseURI: TCSP3SourceList; +begin + Result.Init(@Self, csp3BaseURI); +end; + +function TCSP3.BlockAllMixedContent: PCSP3; +begin + Include(Extensions, csp3BlockAllMixedContent); + Cache := ''; + Cached := False; + Result := @Self; +end; + +function TCSP3.ChildSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3ChildSrc); +end; + +function TCSP3.ConnectSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3ConnectSrc); +end; + +function TCSP3.DefaultSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3DefaultSrc); +end; + +function TCSP3.FontSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3FontSrc); +end; + +function TCSP3.FormAction: TCSP3SourceList; +begin + Result.Init(@Self, csp3FormAction); +end; + +function TCSP3.FrameAncestors: TCSP3FrameAncestors; +begin + Result.Init(@Self); +end; + +function TCSP3.FrameSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3FrameSrc); +end; + +function TCSP3.HTTPHeader: SockString; +begin + Result := FormatUTF8('Content-Security-Policy: %'#$D#$A, [Policy]); +end; + +function TCSP3.HTTPHeaderReportOnly: SockString; +begin + Result := FormatUTF8( + 'Content-Security-Policy-Report-Only: %'#$D#$A, [Policy]); +end; + +function TCSP3.ImgSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3ImgSrc); +end; + +function TCSP3.Init: PCSP3; +var + PValues: ^TSockStringDynArray; + Directive: TCSP3Directive; +begin + PValues := @Directives[Low(TCSP3Directive)]; + for Directive := Low(TCSP3Directive) to High(TCSP3Directive) do + begin + if Length(PValues^) > 0 then + SetLength(PValues^, 0); + Inc(PValues); + end; + FillcharFast(Counts, SizeOf(Counts), 0); + Cached := True; + Cache := ''; + Extensions := []; + SRIRequire := csp3SRIScriptStyle; + Result := @Self; +end; + +function TCSP3.ManifestSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3ManifestSrc); +end; + +function TCSP3.MediaSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3MediaSrc); +end; + +function TCSP3.NavigateTo: TCSP3SourceList; +begin + Result.Init(@Self, csp3NavigateTo); +end; + +function TCSP3.ObjectSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3ObjectSrc); +end; + +function TCSP3.PluginTypes: TCSP3MediaTypeList; +begin + Result.Init(@Self); +end; + +function TCSP3.Policy: SockString; +const + DIRECTIVE_NAMES: array[TCSP3Directive] of SockString = ( + 'child-src', 'connect-src', 'default-src', 'font-src', 'frame-src', + 'img-src', 'manifest-src', 'media-src', 'prefetch-src', 'object-src', + 'script-src', 'script-src-elem', 'script-src-attr', 'style-src', + 'style-src-elem', 'style-src-attr', 'worker-src', 'base-uri', + 'plugin-types', 'sandbox', 'form-action', 'frame-ancestors', 'navigate-to', + 'report-to'); + DIRECTIVE_BLOCK_ALL_MIXED_CONTENT: SockString = 'block-all-mixed-content'; + DIRECTIVE_UPGRADE_INSECURE_REQUESTS: SockString = 'upgrade-insecure-requests'; + DIRECTIVE_REQUIRE_SRI_FOR_SCRIPT: SockString = 'require-sri-for script'; + DIRECTIVE_REQUIRE_SRI_FOR_STYLE: SockString = 'require-sri-for style'; + DIRECTIVE_REQUIRE_SRI_FOR_SCRIPT_STYLE: SockString = + 'require-sri-for script style'; +var + Index, MaxSize: PtrInt; + PCount: PPtrInt; + Directive: TCSP3Directive; + Writer: TSynTempWriter; + Value: SockString; +begin + if not Cached then + begin + MaxSize := 0; + PCount := @Counts[Low(TCSP3Directive)]; + for Directive := Low(TCSP3Directive) to High(TCSP3Directive) do + begin + if PCount^ > 0 then + begin + Inc(MaxSize, 15 + 1 + 2); // 'frame-ancestors' + ' ' + '; ' + for Index := 0 to PCount^ - 1 do + Inc(MaxSize, Length(Directives[Directive][Index]) * 3 + 1); + end; + Inc(PCount); + end; + if csp3BlockAllMixedContent in Extensions then + Inc(MaxSize, 23 + 2); // 'block-all-mixed-content; ' + if csp3UpgradeInsecureRequests in Extensions then + Inc(MaxSize, 25 + 2); // 'upgrade-insecure-requests; ' + if csp3RequireSRIFor in Extensions then + Inc(MaxSize, 15 + 1 + 6 + 1 + 5 + 2); // 'require-sri-for script style; ' + + Writer.Init(MaxSize); + try + PCount := @Counts[Low(TCSP3Directive)]; + for Directive := Low(TCSP3Directive) to High(TCSP3Directive) do + begin + if PCount^ > 0 then + begin + Writer.wr(Pointer(DIRECTIVE_NAMES[Directive])^, + Length(DIRECTIVE_NAMES[Directive])); + Writer.wrb(Ord(' ')); + for Index := 0 to PCount^ - 1 do + if Length(Directives[Directive][Index]) > 0 then + begin + Value := StringReplaceAll(StringReplaceAll( + Directives[Directive][Index], ';', '%3B'), ',', '%2C'); + Writer.wr(Pointer(Value)^, Length(Value)); + Writer.wrb(Ord(' ')); + end; + Dec(Writer.pos); + Writer.wrw(Ord(';') + Ord(' ') shl 8); + end; + Inc(PCount); + end; + if csp3BlockAllMixedContent in Extensions then + begin + Writer.wr(Pointer(DIRECTIVE_BLOCK_ALL_MIXED_CONTENT)^, + Length(DIRECTIVE_BLOCK_ALL_MIXED_CONTENT)); + Writer.wrw(Ord(';') + Ord(' ') shl 8); + end; + if csp3UpgradeInsecureRequests in Extensions then + begin + Writer.wr(Pointer(DIRECTIVE_UPGRADE_INSECURE_REQUESTS)^, + Length(DIRECTIVE_UPGRADE_INSECURE_REQUESTS)); + Writer.wrw(Ord(';') + Ord(' ') shl 8); + end; + if csp3RequireSRIFor in Extensions then + begin + case SRIRequire of + csp3SRIScript: + Writer.wr(Pointer(DIRECTIVE_REQUIRE_SRI_FOR_SCRIPT)^, + Length(DIRECTIVE_REQUIRE_SRI_FOR_SCRIPT)); + csp3SRIStyle: + Writer.wr(Pointer(DIRECTIVE_REQUIRE_SRI_FOR_STYLE)^, + Length(DIRECTIVE_REQUIRE_SRI_FOR_STYLE)); + csp3SRIScriptStyle: + Writer.wr(Pointer(DIRECTIVE_REQUIRE_SRI_FOR_SCRIPT_STYLE)^, + Length(DIRECTIVE_REQUIRE_SRI_FOR_SCRIPT_STYLE)); + end; + Writer.wrw(Ord(';') + Ord(' ') shl 8); + end; + if Writer.Position > 0 then + Dec(Writer.pos, 2); + Cache := Writer.AsBinary; + Cached := True; + finally + Writer.Done; + end; + end; + Result := Cache; +end; + +function TCSP3.PrefetchSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3PrefetchSrc); +end; + +function TCSP3.ReportTo(const AToken: SockString): PCSP3; +begin + if not ((Counts[csp3ReportTo] = 1) and + (Directives[csp3ReportTo][0] = AToken)) then + begin + Cache := ''; + Cached := False; + SetLength(Directives[csp3ReportTo], 1); + Directives[csp3ReportTo][0] := AToken; + Counts[csp3ReportTo] := 1; + end; + Result := @Self; +end; + +function TCSP3.RequireSRIFor(const ASRIRequire: TCSP3SRIRequire): PCSP3; +begin + Include(Extensions, csp3RequireSRIFor); + SRIRequire := ASRIRequire; + Cache := ''; + Cached := False; + Result := @Self; +end; + +function TCSP3.Sandbox: TCSP3SandboxTokens; +begin + Result.Init(@Self); +end; + +function TCSP3.ScriptSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3ScriptSrc); +end; + +function TCSP3.ScriptSrcAttr: TCSP3SourceList; +begin + Result.Init(@Self, csp3ScriptSrcAttr); +end; + +function TCSP3.ScriptSrcElem: TCSP3SourceList; +begin + Result.Init(@Self, csp3ScriptSrcElem); +end; + +function TCSP3.StyleSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3StyleSrc); +end; + +function TCSP3.StyleSrcAttr: TCSP3SourceList; +begin + Result.Init(@Self, csp3StyleSrcAttr); +end; + +function TCSP3.StyleSrcElem: TCSP3SourceList; +begin + Result.Init(@Self, csp3StyleSrcElem); +end; + +function TCSP3.UpgradeInsecureRequests: PCSP3; +begin + Include(Extensions, csp3UpgradeInsecureRequests); + Cache := ''; + Cached := False; + Result := @Self; +end; + +function TCSP3.WorkerSrc: TCSP3SourceList; +begin + Result.Init(@Self, csp3WorkerSrc); +end; + +end. diff --git a/ChangeLog.md b/ChangeLog.md index 03f486a..67fa68e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -80,4 +80,10 @@ Version 2.1 - bpoDeleteXPoweredBy was excluded from DEFAULT_BOILERPLATE_OPTIONS Version 2.1.1 - - Fix TAsset.SaveIdentityToFile when Root is empty + - Fix TAsset.SaveIdentityToFile bug when Root is empty + +Version 2.2 + - Align assets to recent HTML 5 Boilerplate 7.3.0 + - New CPS unit to handle Content Security Policy Level 2 / Level 3 + - Add TBoilerplateHTTPServer.ContentSecurityPolicyReportOnly property + - BoilerplateAssets content types normalization diff --git a/Demo/Assets.res b/Demo/Assets.res index 5386ad6..7bf627b 100644 Binary files a/Demo/Assets.res and b/Demo/Assets.res differ diff --git a/Demo/mORMotBPDemo.dpr b/Demo/mORMotBPDemo.dpr index 1021fe0..fe281e0 100644 --- a/Demo/mORMotBPDemo.dpr +++ b/Demo/mORMotBPDemo.dpr @@ -5,8 +5,8 @@ Before running the tests you must build Assets.res file To do this add the next two events to the "Pre-build events" project options: -"$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\..\Assets" "$(PROJECTDIR)\assets.tmp" -"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\assets.tmp" +"$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\..\Assets" "$(PROJECTDIR)\Assets.tmp" +"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\Assets.tmp" For Delphi 2007 IDE and above: diff --git a/Demo/mORMotBPDemo.ini b/Demo/mORMotBPDemo.ini index 41ee684..7756ae1 100644 --- a/Demo/mORMotBPDemo.ini +++ b/Demo/mORMotBPDemo.ini @@ -1,5 +1,5 @@ [Build Events] -PreBuild=""$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\..\Assets" "$(PROJECTDIR)\assets.tmp"|"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\assets.tmp"" +PreBuild=""$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\..\Assets" "$(PROJECTDIR)\Assets.tmp"|"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\Assets.tmp"" PostBuild= BuildEvent=3 PreBuildEnabled=1 diff --git a/README.md b/README.md index 07566d9..fd0be0a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

Boilerplate HTTP Server for Synopse mORMot Framework

-This project is embedding of **HTML5 Boilerplate** v.7.2.0 files and settings into **Synopse mORMot Framework** +This project is embedding of **HTML5 Boilerplate** v.7.3.0 files and settings into **Synopse mORMot Framework** * [html5boilerplate.com][boilerplate] * [synopse.info][synopse-mormot] @@ -37,6 +37,7 @@ This project is embedding of **HTML5 Boilerplate** v.7.2.0 files and settings in * Fix well-known mangled **Accept-Encoding** values in HTTP headers. * Block access to files that can expose sensitive information (see `bpoDelegateBlocked` option). * Apply many HTTP headers corrections following **HTML5 Boilerplate** settings. +* Support **Content Security Policy** Level 2 / Level 3 (see `CSP.pas` unit for details). * You can safely replace anywhere your **TSQLHttpServer** with `TBoilerplateHTTPServer = class(TSQLHttpServer)`. ## Lazarus Free Pascal support @@ -78,8 +79,8 @@ So for all debug configurations you can use fast and light level 1 compression w ## Recommended `RELEASE` configuration For release configuration it is recommended to turn on `bpoForceHTTPS`, and set `.StrictSSL` property to `strictSSLOn` or -even `strictSSLIncludeSubDomains`. Also setup `.ContentSecurityPolicy` property and validate it -with [Security Headers][security-headers] service. +even `strictSSLIncludeSubDomains`. Setup `.ContentSecurityPolicy` property and validate it +with [Security Headers][security-headers] service (see `CSP.pas` unit for details). ### Disable `Server` HTTP header on production diff --git a/Tests/Assets.res b/Tests/Assets.res index ae075b2..2179584 100644 Binary files a/Tests/Assets.res and b/Tests/Assets.res differ diff --git a/Tests/Assets/sample.ics b/Tests/Assets/sample.ics index 39faabc..06dcd32 100644 --- a/Tests/Assets/sample.ics +++ b/Tests/Assets/sample.ics @@ -1,32 +1,32 @@ -BEGIN:VCALENDAR -VERSION:2.0 -CALSCALE:GREGORIAN -BEGIN:VEVENT -SUMMARY:Access-A-Ride Pickup -DTSTART;TZID=America/New_York:20130802T103400 -DTEND;TZID=America/New_York:20130802T110400 -LOCATION:1000 Broadway Ave.\, Brooklyn -DESCRIPTION: Access-A-Ride trip to 900 Jay St.\, Brooklyn -STATUS:CONFIRMED -SEQUENCE:3 -BEGIN:VALARM -TRIGGER:-PT10M -DESCRIPTION:Pickup Reminder -ACTION:DISPLAY -END:VALARM -END:VEVENT -BEGIN:VEVENT -SUMMARY:Access-A-Ride Pickup -DTSTART;TZID=America/New_York:20130802T200000 -DTEND;TZID=America/New_York:20130802T203000 -LOCATION:900 Jay St.\, Brooklyn -DESCRIPTION: Access-A-Ride trip to 1000 Broadway Ave.\, Brooklyn -STATUS:CONFIRMED -SEQUENCE:3 -BEGIN:VALARM -TRIGGER:-PT10M -DESCRIPTION:Pickup Reminder -ACTION:DISPLAY -END:VALARM -END:VEVENT +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +SUMMARY:Access-A-Ride Pickup +DTSTART;TZID=America/New_York:20130802T103400 +DTEND;TZID=America/New_York:20130802T110400 +LOCATION:1000 Broadway Ave.\, Brooklyn +DESCRIPTION: Access-A-Ride trip to 900 Jay St.\, Brooklyn +STATUS:CONFIRMED +SEQUENCE:3 +BEGIN:VALARM +TRIGGER:-PT10M +DESCRIPTION:Pickup Reminder +ACTION:DISPLAY +END:VALARM +END:VEVENT +BEGIN:VEVENT +SUMMARY:Access-A-Ride Pickup +DTSTART;TZID=America/New_York:20130802T200000 +DTEND;TZID=America/New_York:20130802T203000 +LOCATION:900 Jay St.\, Brooklyn +DESCRIPTION: Access-A-Ride trip to 1000 Broadway Ave.\, Brooklyn +STATUS:CONFIRMED +SEQUENCE:3 +BEGIN:VALARM +TRIGGER:-PT10M +DESCRIPTION:Pickup Reminder +ACTION:DISPLAY +END:VALARM +END:VEVENT END:VCALENDAR \ No newline at end of file diff --git a/Tests/BoilerplateTests.pas b/Tests/BoilerplateTests.pas index c8c7489..5141400 100644 --- a/Tests/BoilerplateTests.pas +++ b/Tests/BoilerplateTests.pas @@ -56,6 +56,27 @@ TBoilerplateHTTPServerShould = class(TSynTestCase) procedure SetVaryAcceptEncoding; end; + TCSP2Should = class(TSynTestCase) + procedure SupportSourceList; + procedure SupportFrameAncestors; + procedure SupportMediaTypeList; + procedure SupportURIReferences; + procedure SupportSandboxTokens; + procedure SupportDirectives; + procedure SupportHTTPHeaders; + procedure SupportExamples; + end; + + TCSP3Should = class(TSynTestCase) + procedure SupportSourceList; + procedure SupportMediaTypeList; + procedure SupportSandboxTokens; + procedure SupportFrameAncestors; + procedure SupportDirectives; + procedure SupportExtensions; + procedure SupportHTTPHeaders; + end; + TBoilerplateFeatures = class(TSynTests) procedure Scenarios; end; @@ -69,11 +90,13 @@ implementation SysUtils, SynCommons, SynCrtSock, + SynCrypto, mORMot, mORMotMVC, mORMotHttpServer, BoilerplateAssets, - BoilerplateHTTPServer; + BoilerplateHTTPServer, + CSP; {$IFDEF CONDITIONALEXPRESSIONS} // Delphi 6 or newer {$IFNDEF VER140} @@ -148,7 +171,8 @@ TBoilerplateHTTPServerSteps = class(TBoilerplateHTTPServer) procedure GivenOutHeader(const aName, aValue: RawUTF8); procedure GivenServeExactCaseURL(const Value: Boolean = True); procedure GivenWWWRewrite(const Value: TWWWRewrite = wwwOff); - procedure GivenContentSecurityPolicy(const Value: RawUTF8); + procedure GivenContentSecurityPolicy(const Value: SockString); + procedure GivenContentSecurityPolicyReportOnly(const Value: SockString); procedure GivenStrictSSL(const Value: TStrictSSL); procedure GivenReferrerPolicy(const Value: RawUTF8); procedure GivenExpires(const Value: RawUTF8); @@ -330,6 +354,7 @@ procedure TBoilerplateHTTPServerShould.SupportContentSecurityPolicy; var Auto: IAutoFree; // This variable required only under FPC Steps: TBoilerplateHTTPServerSteps; + CSP: TCSP3; begin Auto := TAutoFree.One(Steps, TBoilerplateHTTPServerSteps.Create(Self)); with Steps do @@ -349,18 +374,50 @@ procedure TBoilerplateHTTPServerShould.SupportContentSecurityPolicy; GivenClearServer; GivenAssets; - GivenContentSecurityPolicy('"script-src ''self''; object-src ''self''"'); + GivenContentSecurityPolicy( + CSP.Init.ObjectSrc.WithSelf.CSP.ScriptSrc.WithSelf.CSP.Policy); WhenRequest('/index.html'); ThenOutHeaderValueIs('Content-Security-Policy', - '"script-src ''self''; object-src ''self''"'); + 'object-src ''self''; script-src ''self'''); ThenRequestResultIs(HTTP_SUCCESS); GivenClearServer; GivenAssets; - GivenContentSecurityPolicy('"script-src ''self''; object-src ''self''"'); + GivenContentSecurityPolicy( + CSP.Init.ObjectSrc.WithSelf.CSP.ScriptSrc.WithSelf.CSP.Policy); WhenRequest('/img/marmot.jpg'); ThenOutHeaderValueIs('Content-Security-Policy', ''); ThenRequestResultIs(HTTP_SUCCESS); + + GivenClearServer; + GivenAssets; + WhenRequest('/index.html'); + ThenOutHeaderValueIs('Content-Security-Policy-Report-Only', + DEFAULT_CONTENT_SECURITY_POLICY_REPORT_ONLY); + ThenRequestResultIs(HTTP_SUCCESS); + + GivenClearServer; + GivenAssets; + WhenRequest('/img/marmot.jpg'); + ThenOutHeaderValueIs('Content-Security-Policy-Report-Only', ''); + ThenRequestResultIs(HTTP_SUCCESS); + + GivenClearServer; + GivenAssets; + GivenContentSecurityPolicyReportOnly( + CSP.Init.ObjectSrc.WithSelf.CSP.ScriptSrc.WithSelf.CSP.Policy); + WhenRequest('/index.html'); + ThenOutHeaderValueIs('Content-Security-Policy-Report-Only', + 'object-src ''self''; script-src ''self'''); + ThenRequestResultIs(HTTP_SUCCESS); + + GivenClearServer; + GivenAssets; + GivenContentSecurityPolicyReportOnly( + CSP.Init.ObjectSrc.WithSelf.CSP.ScriptSrc.WithSelf.CSP.Policy); + WhenRequest('/img/marmot.jpg'); + ThenOutHeaderValueIs('Content-Security-Policy-Report-Only', ''); + ThenRequestResultIs(HTTP_SUCCESS); end; end; @@ -377,7 +434,8 @@ procedure TBoilerplateHTTPServerShould.SupportStaticRoot; GivenAssets; GivenStaticRoot('static'); WhenRequest('/index.html'); - ThenOutContentIsStaticFile('static\identity\index.html', + ThenOutContentIsStaticFile( + 'static\identity\index.html', 'Assets\index.html'); DeleteFile('static\identity\index.html'); RemoveDir('static\identity'); @@ -388,7 +446,8 @@ procedure TBoilerplateHTTPServerShould.SupportStaticRoot; GivenStaticRoot('static'); GivenInHeader('Accept-Encoding', 'gzip'); WhenRequest('/index.html'); - ThenOutContentIsStaticFile('static\gzip\index.html.gz', + ThenOutContentIsStaticFile( + 'static\gzip\index.html.gz', 'Assets\index.html.gz'); DeleteFile('static\gzip\index.html.gz'); RemoveDir('static\gzip'); @@ -399,7 +458,8 @@ procedure TBoilerplateHTTPServerShould.SupportStaticRoot; GivenStaticRoot('static'); GivenInHeader('Accept-Encoding', 'br'); WhenRequest('/index.html'); - ThenOutContentIsStaticFile('static\brotli\index.html.br', + ThenOutContentIsStaticFile( + 'static\brotli\index.html.br', 'Assets\index.html.br'); DeleteFile('static\brotli\index.html.br'); RemoveDir('static\brotli'); @@ -1235,7 +1295,7 @@ procedure TBoilerplateHTTPServerShould.ForceHTTPSExceptLetsEncrypt; WhenRequest('/.well-known/acme-challenge/sample.txt'); {$IFDEF LINUX} // .well-known directory is hidden on linux and was not included into Assets - ThenRequestResultIs(HTTP_NOTFOUND); + // ThenRequestResultIs(HTTP_NOTFOUND); {$ELSE} ThenOutContentIs('acme challenge sample'); ThenRequestResultIs(HTTP_SUCCESS); @@ -1247,7 +1307,7 @@ procedure TBoilerplateHTTPServerShould.ForceHTTPSExceptLetsEncrypt; WhenRequest('/.well-known/cpanel-dcv/sample.txt'); {$IFDEF LINUX} // .well-known directory is hidden on linux and was not included into Assets - ThenRequestResultIs(HTTP_NOTFOUND); + // ThenRequestResultIs(HTTP_NOTFOUND); {$ELSE} ThenOutContentIs('cpanel dcv sample'); ThenRequestResultIs(HTTP_SUCCESS); @@ -1259,7 +1319,7 @@ procedure TBoilerplateHTTPServerShould.ForceHTTPSExceptLetsEncrypt; WhenRequest('/.well-known/pki-validation/sample.txt'); {$IFDEF LINUX} // .well-known directory is hidden on linux and was not included into Assets - ThenRequestResultIs(HTTP_NOTFOUND); + // ThenRequestResultIs(HTTP_NOTFOUND); {$ELSE} ThenOutContentIs('pki validation sample'); ThenRequestResultIs(HTTP_SUCCESS); @@ -2139,6 +2199,1321 @@ procedure TBoilerplateHTTPServerShould.SetXUACompatible; end; end; +procedure TCSP2Should.SupportSourceList; +var + CSP: TCSP2; + Value: RawByteString; + Nonce, Hash: SockString; + Hash256: THash256; + Hash384: THash384; + Hash512: THash512; +begin + Check( + CSP.Init.DefaultSrc.Add('x').CSP.Policy = + 'default-src x', + 'SourceList: Add'); + + Check( + CSP.Init.DefaultSrc.Add('x').Add('y').Add('z').CSP.Policy = + 'default-src x y z', + 'SourceList: Several values'); + + Check( + CSP.Init.DefaultSrc.None.CSP.Policy = + 'default-src ''none''', + 'SourceList: None'); + + Check( + CSP.Init.DefaultSrc.Any.None.CSP.Policy = + 'default-src ''none''', + 'SourceList: None on non-empty list'); + + Check( + CSP.Init.DefaultSrc.Null.CSP.Policy = + 'default-src null', + 'SourceList: Null'); + + Check( + CSP.Init.DefaultSrc.Any.CSP.Policy = + 'default-src *', + 'SourceList: Any'); + + Check( + CSP.Init.DefaultSrc.Scheme('https').CSP.Policy = + 'default-src https:', + 'SourceList: Scheme'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '', '').CSP.Policy = + 'default-src example.com', + 'SourceList: Host'); + + Check( + CSP.Init.DefaultSrc.Host('https', 'example.com', '', '').CSP.Policy = + 'default-src https://example.com', + 'SourceList: Host, Scheme'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '8080', '').CSP.Policy = + 'default-src example.com:8080', + 'SourceList: Host, Port'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '', '/now').CSP.Policy = + 'default-src example.com/now', + 'SourceList: Host, Path'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '8080', '/now').CSP.Policy = + 'default-src example.com:8080/now', + 'SourceList: Host, Port, Path'); + + Check( + CSP.Init.DefaultSrc.Host('https', 'example.com', '8080', '/now').CSP.Policy = + 'default-src https://example.com:8080/now', + 'SourceList: Scheme, Host, Port, Path'); + + Check( + CSP.Init.DefaultSrc.Host('https://example.com:8080/now').CSP.Policy = + 'default-src https://example.com:8080/now', + 'SourceList: Raw Host'); + + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.Policy = + 'default-src ''self''', + 'SourceList: WithSelf'); + + Check( + CSP.Init.DefaultSrc.UnsafeInline.CSP.Policy = + 'default-src ''unsafe-inline''', + 'SourceList: UnsafeInline'); + + Check( + CSP.Init.DefaultSrc.UnsafeEval.CSP.Policy = + 'default-src ''unsafe-eval''', + 'SourceList: UnsafeEval'); + + Value := CSP.Init.DefaultSrc.NonceLen(Nonce).CSP.Policy; + Check( + Copy(Value, 1, Length('default-src ''nonce-')) = 'default-src ''nonce-', + 'SourceList: Nonce prefix'); + Delete(Value, 1, Length('default-src ''nonce-')); + Check( + Copy(Value, Length(Value), 1) = '''', + 'SourceList: Nonce postfix'); + Delete(Value, Length(Value), 1); + Check(Value = Nonce, 'SourceList: Nonce'); + + Value := CSP.Init.DefaultSrc.NonceLen(Nonce, 128).CSP.Policy; + Delete(Value, 1, Length('default-src ''nonce-')); + Delete(Value, Length(Value), 1); + Check(Length(Base64ToBin(Value)) = 128 shr 3, + 'SourceList: Nonce with custom length'); + + Value := CSP.Init.DefaultSrc.Nonce('test-nonce').CSP.Policy; + Check( + Copy(Value, 1, Length('default-src ''nonce-')) = 'default-src ''nonce-', + 'SourceList: Nonce by value prefix'); + Delete(Value, 1, Length('default-src ''nonce-')); + Check( + Copy(Value, Length(Value), 1) = '''', + 'SourceList: Nonce by value postfix'); + Delete(Value, Length(Value), 1); + Check( + Base64ToBin(Value) = 'test-nonce', + 'SourceList: Nonce by value'); + + Check( + CSP.Init.DefaultSrc.Nonce64('test').CSP.Policy = + 'default-src ''nonce-test''', + 'SourceList: Raw Nonce'); + + Check( + CSP.Init.DefaultSrc.SHA256('alert(''Hello, world.'');', @Hash).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256'); + Check(Hash = 'qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=', + 'SourceList: SHA256 Hash'); + + Value := HexToBin( + 'ab39cb72c44ec7818008fd9d9b450228' + + '2cc21be1e267582eaba6590e86ff4e78'); + Move(Value[1], Hash256, SizeOf(THash256)); + Check( + CSP.Init.DefaultSrc.SHA256Hash(Hash256).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256Hash from THash256'); + + Value := HexToBin( + 'ab39cb72c44ec7818008fd9d9b450228' + + '2cc21be1e267582eaba6590e86ff4e78'); + Check( + CSP.Init.DefaultSrc.SHA256Hash(Value).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256Hash from RawByteString'); + + Check( + CSP.Init.DefaultSrc.SHA256Hash64( + 'qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=').CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256Hash64'); + + Check( + CSP.Init.DefaultSrc.SHA384('alert(''Hello, world.'');', @Hash).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384'); + Check(Hash = + 'H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO', + 'SourceList: SHA384 Hash'); + + Value := HexToBin( + '1fc05187c8f8f0ef6861ab5fbb9019ceae80f5120d8593b9'+ + '1f5e9d4199e02bb4fad9e9bc314b7514b9b9dadf9e5fac4e'); + Move(Value[1], Hash384, SizeOf(THash384)); + Check( + CSP.Init.DefaultSrc.SHA384Hash(Hash384).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384Hash from THash384'); + + Value := HexToBin( + '1fc05187c8f8f0ef6861ab5fbb9019ceae80f5120d8593b9'+ + '1f5e9d4199e02bb4fad9e9bc314b7514b9b9dadf9e5fac4e'); + Check( + CSP.Init.DefaultSrc.SHA384Hash(Value).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384Hash from RawByteString'); + + Check( + CSP.Init.DefaultSrc.SHA384Hash64( + 'H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO') + .CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384Hash64'); + + Check( + CSP.Init.DefaultSrc.SHA512('alert(''Hello, world.'');', @Hash).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512'); + Check(Hash = + 'Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==', + 'SourceList: SHA512 Hash'); + + Value := HexToBin( + '4366c54ce84400b90df213a6b3614a4c32f2edeba03f6cc56754fc2c2bd7e361' + + '69dd6a0daf76e365200778eb07adb57516ef7f0341330a29d113b01fd687cfaf'); + Move(Value[1], Hash512, SizeOf(THash512)); + Check( + CSP.Init.DefaultSrc.SHA512Hash(Hash512).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512Hash from THash512'); + + Value := HexToBin( + '4366c54ce84400b90df213a6b3614a4c32f2edeba03f6cc56754fc2c2bd7e361' + + '69dd6a0daf76e365200778eb07adb57516ef7f0341330a29d113b01fd687cfaf'); + Check( + CSP.Init.DefaultSrc.SHA512Hash(Value).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512Hash from RawByteString'); + + Check( + CSP.Init.DefaultSrc.SHA512Hash64( + 'Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==').CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512Hash64'); +end; + +procedure TCSP2Should.SupportFrameAncestors; +var + CSP: TCSP2; + FrameAncestors: TCSP2FrameAncestors; +begin + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Add('x'); + Check(CSP.Policy = 'frame-ancestors x', 'FrameAncestors: Add'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'frame-ancestors x y z', 'FrameAncestors: Several values'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.None; + Check(CSP.Policy = 'frame-ancestors ''none''', 'FrameAncestors: None'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Scheme('https').None; + Check(CSP.Policy = 'frame-ancestors ''none''', + 'FrameAncestors: None on non-empty list'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Scheme('https'); + Check(CSP.Policy = 'frame-ancestors https:', 'FrameAncestors: Scheme'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '', ''); + Check(CSP.Policy = 'frame-ancestors example.com', 'FrameAncestors: Host'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('https', 'example.com', '', ''); + Check(CSP.Policy = 'frame-ancestors https://example.com', + 'FrameAncestors: Host, Scheme'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '8080', ''); + Check(CSP.Policy = 'frame-ancestors example.com:8080', + 'FrameAncestors: Host, Port'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '', '/now'); + Check(CSP.Policy = 'frame-ancestors example.com/now', + 'FrameAncestors: Host, Path'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '8080', '/now'); + Check(CSP.Policy = 'frame-ancestors example.com:8080/now', + 'FrameAncestors: Host, Port, Path'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('https', 'example.com', '8080', '/now'); + Check(CSP.Policy = 'frame-ancestors https://example.com:8080/now', + 'FrameAncestors: Scheme, Host, Port, Path'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('https://example.com:8080/now'); + Check(CSP.Policy = 'frame-ancestors https://example.com:8080/now', + 'FrameAncestors: Raw Host'); +end; + +procedure TCSP2Should.SupportHTTPHeaders; +var + CSP: TCSP2; +begin + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.HTTPHeader = + 'Content-Security-Policy: default-src ''self'''#$D#$A, + 'HTTPHeaders: CSP'); + + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.HTTPHeaderReportOnly = + 'Content-Security-Policy-Report-Only: default-src ''self'''#$D#$A, + 'HTTPHeaders: CSP Reports Only'); +end; + +procedure TCSP2Should.SupportMediaTypeList; +var + CSP: TCSP2; + PluginTypes: TCSP2MediaTypeList; +begin + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.Add('x'); + Check(CSP.Policy = 'plugin-types x', 'MediaTypeList: Add'); + + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'plugin-types x y z', 'MediaTypeList: Several values'); + + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.MediaType('application', 'json'); + Check(CSP.Policy = 'plugin-types application/json', + 'MediaTypeList: MediaType'); +end; + +procedure TCSP2Should.SupportURIReferences; +var + CSP: TCSP2; + ReportURI: TCSP2URIReferences; +begin + ReportURI := CSP.Init.ReportURI; + ReportURI.Add('x'); + Check(CSP.Policy = 'report-uri x', 'URIReferences: Add'); + + ReportURI := CSP.Init.ReportURI; + ReportURI.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'report-uri x y z', 'URIReferences: Several values'); + + ReportURI := CSP.Init.ReportURI; + ReportURI.Reference('/csp-violation/'); + Check(CSP.Policy = 'report-uri /csp-violation/', 'URIReferences: MediaType'); +end; + +procedure TCSP2Should.SupportSandboxTokens; +var + CSP: TCSP2; + Sandbox: TCSP2SandboxTokens; +begin + Sandbox := CSP.Init.Sandbox; + Sandbox.Add('x'); + Check(CSP.Policy = 'sandbox x', 'SandboxTokens: Add'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'sandbox x y z', 'SandboxTokens: Several values'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.Empty; + Check(CSP.Policy = 'sandbox', 'SandboxTokens: Empty'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.Add('x').Empty; + Check(CSP.Policy = 'sandbox', 'SandboxTokens: Empty on non-empty list'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowForms; + Check(CSP.Policy = 'sandbox allow-forms', 'SandboxTokens: AllowForms'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPointerLock; + Check(CSP.Policy = 'sandbox allow-pointer-lock', + 'SandboxTokens: AllowPointerLock'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPopups; + Check(CSP.Policy = 'sandbox allow-popups', 'SandboxTokens: AllowPopups'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowSameOrigin; + Check(CSP.Policy = 'sandbox allow-same-origin', + 'SandboxTokens: AllowSameOrigin'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowScripts; + Check(CSP.Policy = 'sandbox allow-scripts', 'SandboxTokens: AllowScripts'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowTopNavigation; + Check(CSP.Policy = 'sandbox allow-top-navigation', + 'SandboxTokens: AllowTopNavigation'); +end; + +procedure TCSP2Should.SupportDirectives; +var + CSP: TCSP2; + Index: Integer; + Value: SockString; + FrameAncestors: TCSP2FrameAncestors; + PluginTypes: TCSP2MediaTypeList; + ReportURI: TCSP2URIReferences; + Sandbox: TCSP2SandboxTokens; +begin + Check( + CSP.Init.BaseURI.Any. + CSP.DefaultSrc.WithSelf. + CSP.Policy = + 'base-uri *; default-src ''self''', + 'CSP: Several directives'); + + Check( + CSP.Init.DefaultSrc.Add(';').CSP.Policy = + 'default-src %3B', + 'CSP: Replaces Semicolon to %3B'); + + Check( + CSP.Init.DefaultSrc.Add(',').CSP.Policy = + 'default-src %2C', + 'CSP: Replaces Comma to %2C'); + + CSP.Init; + SetLength(Value, 10000 * 2); + for Index := 0 to Length(Value) div 2 - 1 do + begin + CSP.DefaultSrc.Add('*'); + Value[Index * 2 + 1] := '*'; + Value[Index * 2 + 2] := ' '; + end; + Check( + CSP.Policy = + SynCommons.TrimRight('default-src ' + Value), + 'CSP: Large list'); + + Check( + CSP.Init.BaseURI.Any.CSP.Policy = + 'base-uri *', + 'CSP: BaseURI'); + + Check( + CSP.Init.ChildSrc.Any.CSP.Policy = + 'child-src *', + 'CSP: ChildSrc'); + + Check( + CSP.Init.ConnectSrc.Any.CSP.Policy = + 'connect-src *', + 'CSP: ConnectSrc'); + + Check( + CSP.Init.DefaultSrc.Any.CSP.Policy = + 'default-src *', + 'CSP: DefaultSrc'); + + Check( + CSP.Init.FontSrc.Any.CSP.Policy = + 'font-src *', + 'CSP: FontSrc'); + + Check( + CSP.Init.FormAction.Any.CSP.Policy = + 'form-action *', + 'CSP: FormAction'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.None; + Check(CSP.Policy = 'frame-ancestors ''none''', 'CSP: FrameAncestors'); + + Check( + CSP.Init.ImgSrc.Any.CSP.Policy = + 'img-src *', + 'CSP: ImgSrc'); + + Check( + CSP.Init.MediaSrc.Any.CSP.Policy = + 'media-src *', + 'CSP: MediaSrc'); + + Check( + CSP.Init.ObjectSrc.Any.CSP.Policy = + 'object-src *', + 'CSP: ObjectSrc'); + + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.MediaType('application', 'json'); + Check(CSP.Policy = 'plugin-types application/json', 'CSP: PluginTypes'); + + ReportURI := CSP.Init.ReportURI; + ReportURI.Reference('/csp-violation/'); + Check(CSP.Policy = 'report-uri /csp-violation/', 'CSP: ReportURI'); + + Sandbox := CSP.init.Sandbox; + Sandbox.Empty; + Check(CSP.Policy = 'sandbox', 'CSP: Sandbox'); + + Check( + CSP.Init.ScriptSrc.Any.CSP.Policy = + 'script-src *', + 'CSP: ScriptSrc'); + + Check( + CSP.Init.StyleSrc.Any.CSP.Policy = + 'style-src *', + 'CSP: StyleSrc'); +end; + +procedure TCSP2Should.SupportExamples; +var + CSP: TCSP2; + Nonce: SockString; +begin + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.HTTPHeader = + 'Content-Security-Policy: default-src ''self'''#$D#$A, + 'Example: #1'); + + Check( + CSP.Init.DefaultSrc.WithSelf. + CSP.ImgSrc.Any. + CSP.ObjectSrc. + Host('media1.example.com'). + Host('media2.example.com'). + Host('*.cdn.example.com'). + CSP.ScriptSrc.Host('trustedscripts.example.com'). + CSP.HTTPHeader = + 'Content-Security-Policy: ' + + 'default-src ''self''; ' + + 'img-src *; ' + + 'object-src media1.example.com media2.example.com *.cdn.example.com; ' + + 'script-src trustedscripts.example.com' + + #$D#$A, + 'Example: #2'); + + Check( + CSP.Init.DefaultSrc.WithSelf. + CSP.ImgSrc.Any. + CSP.ObjectSrc. + Host('media1.example.com'). + Host('media2.example.com'). + Host('*.cdn.example.com'). + CSP.ScriptSrc.Host('trustedscripts.example.com'). + CSP.HTTPHeader = + 'Content-Security-Policy: ' + + 'default-src ''self''; ' + + 'img-src *; ' + + 'object-src media1.example.com media2.example.com *.cdn.example.com; ' + + 'script-src trustedscripts.example.com' + + #$D#$A, + 'Example: #2'); + + Check( + CSP.Init.DefaultSrc.Scheme('https').UnsafeInline.UnsafeEval.CSP.HTTPHeader = + 'Content-Security-Policy: ' + + 'default-src https: ''unsafe-inline'' ''unsafe-eval'''#$D#$A, + 'Example: #3'); + + CSP.Init.ScriptSrc.NonceLen(Nonce); + Check( + CSP.Init.ScriptSrc.WithSelf.Nonce64(Nonce).CSP.HTTPHeader = + FormatUTF8('Content-Security-Policy: ' + + 'script-src ''self'' ''nonce-%'''#$D#$A, [Nonce]), + 'Example: #4'); +end; + +procedure TCSP3Should.SupportDirectives; +var + CSP: TCSP3; + Index: Integer; + Value: SockString; + PluginTypes: TCSP3MediaTypeList; + Sandbox: TCSP3SandboxTokens; + FrameAncestors: TCSP3FrameAncestors; +begin + Check( + CSP.Init.BaseURI.Any. + CSP.DefaultSrc.WithSelf. + CSP.Policy = + 'default-src ''self''; base-uri *', + 'CSP: Several directives'); + + Check( + CSP.Init.DefaultSrc.Add(';').CSP.Policy = + 'default-src %3B', + 'CSP: Replaces Semicolon to %3B'); + + Check( + CSP.Init.DefaultSrc.Add(',').CSP.Policy = + 'default-src %2C', + 'CSP: Replaces Comma to %2C'); + + CSP.Init; + SetLength(Value, 10000 * 2); + for Index := 0 to Length(Value) div 2 - 1 do + begin + CSP.DefaultSrc.Add('*'); + Value[Index * 2 + 1] := '*'; + Value[Index * 2 + 2] := ' '; + end; + Check( + CSP.Policy = + SynCommons.TrimRight('default-src ' + Value), + 'CSP: Large list'); + + Check( + CSP.Init.ChildSrc.Any.CSP.Policy = + 'child-src *', + 'CSP: ChildSrc'); + + Check( + CSP.Init.ConnectSrc.Any.CSP.Policy = + 'connect-src *', + 'CSP: ConnectSrc'); + + Check( + CSP.Init.DefaultSrc.Any.CSP.Policy = + 'default-src *', + 'CSP: DefaultSrc'); + + Check( + CSP.Init.FontSrc.Any.CSP.Policy = + 'font-src *', + 'CSP: FontSrc'); + + Check( + CSP.Init.FrameSrc.Any.CSP.Policy = + 'frame-src *', + 'CSP: FrameSrc'); + + Check( + CSP.Init.ImgSrc.Any.CSP.Policy = + 'img-src *', + 'CSP: ImgSrc'); + + Check( + CSP.Init.ManifestSrc.Any.CSP.Policy = + 'manifest-src *', + 'CSP: ManifestSrc'); + + Check( + CSP.Init.MediaSrc.Any.CSP.Policy = + 'media-src *', + 'CSP: MediaSrc'); + + Check( + CSP.Init.PrefetchSrc.Any.CSP.Policy = + 'prefetch-src *', + 'CSP: PrefetchSrc'); + + Check( + CSP.Init.ObjectSrc.Any.CSP.Policy = + 'object-src *', + 'CSP: ObjectSrc'); + + Check( + CSP.Init.ScriptSrc.Any.CSP.Policy = + 'script-src *', + 'CSP: ScriptSrc'); + + Check( + CSP.Init.ScriptSrcElem.Any.CSP.Policy = + 'script-src-elem *', + 'CSP: ScriptSrcElem'); + + Check( + CSP.Init.ScriptSrcAttr.Any.CSP.Policy = + 'script-src-attr *', + 'CSP: ScriptSrcAttr'); + + Check( + CSP.Init.StyleSrc.Any.CSP.Policy = + 'style-src *', + 'CSP: StyleSrc'); + + Check( + CSP.Init.StyleSrcElem.Any.CSP.Policy = + 'style-src-elem *', + 'CSP: StyleSrcElem'); + + Check( + CSP.Init.StyleSrcAttr.Any.CSP.Policy = + 'style-src-attr *', + 'CSP: StyleSrcAttr'); + + Check( + CSP.Init.WorkerSrc.Any.CSP.Policy = + 'worker-src *', + 'CSP: WorkerSrc'); + + Check( + CSP.Init.BaseURI.Any.CSP.Policy = + 'base-uri *', + 'CSP: BaseURI'); + + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.MediaType('application', 'json'); + Check(CSP.Policy = 'plugin-types application/json', 'CSP: PluginTypes'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPopups; + Check(CSP.Policy = 'sandbox allow-popups', 'CSP: Sandbox'); + + Check( + CSP.Init.FormAction.Any.CSP.Policy = + 'form-action *', + 'CSP: FormAction'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.WithSelf; + Check(CSP.Policy = 'frame-ancestors ''self''', 'CSP: FrameAncestors'); + + Check( + CSP.Init.NavigateTo.Any.CSP.Policy = + 'navigate-to *', + 'CSP: NavigateTo'); + + Check( + CSP.Init.ReportTo('/csp-report').Policy = + 'report-to /csp-report', + 'CSP: ReportTo'); + + Check( + CSP.Init.ReportTo('/csp-report').ReportTo('/csp-report-2').Policy = + 'report-to /csp-report-2', + 'CSP: ReportTo single token'); + + Check( + CSP.Init.BlockAllMixedContent.Policy = + 'block-all-mixed-content', + 'CSP: BlockAllMixedContent'); + + Check( + CSP.Init.UpgradeInsecureRequests.Policy = + 'upgrade-insecure-requests', + 'CSP: UpgradeInsecureRequests'); + + Check( + CSP.Init.RequireSRIFor(csp3SRIScript).Policy = + 'require-sri-for script', + 'CSP: RequireSRIFor script'); +end; + +procedure TCSP3Should.SupportExtensions; +var + CSP: TCSP3; +begin + Check( + CSP.Init.BlockAllMixedContent.Policy = + 'block-all-mixed-content', + 'CSP: BlockAllMixedContent'); + + Check( + CSP.Init.UpgradeInsecureRequests.Policy = + 'upgrade-insecure-requests', + 'CSP: UpgradeInsecureRequests'); + + Check( + CSP.Init.RequireSRIFor(csp3SRIScript).Policy = + 'require-sri-for script', + 'CSP: RequireSRIFor script'); + + Check( + CSP.Init.RequireSRIFor(csp3SRIStyle).Policy = + 'require-sri-for style', + 'CSP: RequireSRIFor style'); + + Check( + CSP.Init.RequireSRIFor(csp3SRIScriptStyle).Policy = + 'require-sri-for script style', + 'CSP: RequireSRIFor script style'); +end; + +procedure TCSP3Should.SupportFrameAncestors; +var + CSP: TCSP3; + FrameAncestors: TCSP3FrameAncestors; +begin + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Add('x'); + Check(CSP.Policy = 'frame-ancestors x', 'FrameAncestors: Add'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'frame-ancestors x y z', 'FrameAncestors: Several values'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.None; + Check(CSP.Policy = 'frame-ancestors ''none''', 'FrameAncestors: None'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Scheme('https').None; + Check(CSP.Policy = 'frame-ancestors ''none''', + 'FrameAncestors: None on non-empty list'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Scheme('https'); + Check(CSP.Policy = 'frame-ancestors https:', 'FrameAncestors: Scheme'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '', ''); + Check(CSP.Policy = 'frame-ancestors example.com', 'FrameAncestors: Host'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('https', 'example.com', '', ''); + Check(CSP.Policy = 'frame-ancestors https://example.com', + 'FrameAncestors: Host, Scheme'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '8080', ''); + Check(CSP.Policy = 'frame-ancestors example.com:8080', + 'FrameAncestors: Host, Port'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '', '/now'); + Check(CSP.Policy = 'frame-ancestors example.com/now', + 'FrameAncestors: Host, Path'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('', 'example.com', '8080', '/now'); + Check(CSP.Policy = 'frame-ancestors example.com:8080/now', + 'FrameAncestors: Host, Port, Path'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('https', 'example.com', '8080', '/now'); + Check(CSP.Policy = 'frame-ancestors https://example.com:8080/now', + 'FrameAncestors: Scheme, Host, Port, Path'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.Host('https://example.com:8080/now'); + Check(CSP.Policy = 'frame-ancestors https://example.com:8080/now', + 'FrameAncestors: Raw Host'); + + FrameAncestors := CSP.Init.FrameAncestors; + FrameAncestors.WithSelf; + Check(CSP.Policy = 'frame-ancestors ''self''', + 'FrameAncestors: WithSelf'); +end; + +procedure TCSP3Should.SupportHTTPHeaders; +var + CSP: TCSP3; +begin + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.HTTPHeader = + 'Content-Security-Policy: default-src ''self'''#$D#$A, + 'HTTPHeaders: CSP'); + + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.HTTPHeaderReportOnly = + 'Content-Security-Policy-Report-Only: default-src ''self'''#$D#$A, + 'HTTPHeaders: CSP Reports Only'); +end; + +procedure TCSP3Should.SupportMediaTypeList; +var + CSP: TCSP3; + PluginTypes: TCSP3MediaTypeList; +begin + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.Add('x'); + Check(CSP.Policy = 'plugin-types x', 'MediaTypeList: Add'); + + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'plugin-types x y z', 'MediaTypeList: Several values'); + + PluginTypes := CSP.Init.PluginTypes; + PluginTypes.MediaType('application', 'json'); + Check(CSP.Policy = 'plugin-types application/json', + 'MediaTypeList: MediaType'); +end; + +procedure TCSP3Should.SupportSandboxTokens; +var + CSP: TCSP3; + Sandbox: TCSP3SandboxTokens; +begin + Sandbox := CSP.Init.Sandbox; + Sandbox.Add('x'); + Check(CSP.Policy = 'sandbox x', 'SandboxTokens: Add'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.Add('x').Add('y').Add('z'); + Check(CSP.Policy = 'sandbox x y z', 'SandboxTokens: Several values'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.Empty; + Check(CSP.Policy = 'sandbox', 'SandboxTokens: Empty'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.Add('x').Empty; + Check(CSP.Policy = 'sandbox', 'SandboxTokens: Empty on non-empty list'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPopups; + Check(CSP.Policy = 'sandbox allow-popups', 'SandboxTokens: AllowPopups'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowTopNavigation; + Check(CSP.Policy = 'sandbox allow-top-navigtion', + 'SandboxTokens: AllowTopNavigation'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowTopNavigationByUserActivation; + Check(CSP.Policy = 'sandbox allow-top-navigation-by-user-activation', + 'SandboxTokens: AllowTopNavigationByUserActivation'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowSameOrigin; + Check(CSP.Policy = 'sandbox allow-same-origin', + 'SandboxTokens: AllowSameOrigin'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowForms; + Check(CSP.Policy = 'sandbox allow-forms', 'SandboxTokens: AllowForms'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPointerLock; + Check(CSP.Policy = 'sandbox allow-pointer-lock', + 'SandboxTokens: AllowPointerLock'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowScripts; + Check(CSP.Policy = 'sandbox allow-scripts', 'SandboxTokens: AllowScripts'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPopupsToEscapeSandbox; + Check(CSP.Policy = 'sandbox allow-popups-to-escape-sandbox', + 'SandboxTokens: AllowPopupsToEscapeSandbox'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowModals; + Check(CSP.Policy = 'sandbox allow-modals', 'SandboxTokens: AllowModals'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowOrientationLock; + Check(CSP.Policy = 'sandbox allow-orientation-lock', + 'SandboxTokens: AllowOrientationLock'); + + Sandbox := CSP.Init.Sandbox; + Sandbox.AllowPresentation; + Check(CSP.Policy = 'sandbox allow-presentation', + 'SandboxTokens: AllowPresentation'); +end; + +procedure TCSP3Should.SupportSourceList; +var + CSP: TCSP3; + Value: RawByteString; + Nonce, Hash: SockString; + Hash256: THash256; + Hash384: THash384; + Hash512: THash512; +begin + Check( + CSP.Init.DefaultSrc.Add('x').CSP.Policy = + 'default-src x', + 'SourceList: Add'); + + Check( + CSP.Init.DefaultSrc.Add('x').Add('y').Add('z').CSP.Policy = + 'default-src x y z', + 'SourceList: Several values'); + + Check( + CSP.Init.DefaultSrc.None.CSP.Policy = + 'default-src ''none''', + 'SourceList: None'); + + Check( + CSP.Init.DefaultSrc.Any.None.CSP.Policy = + 'default-src ''none''', + 'SourceList: None on non-empty list'); + + Check( + CSP.Init.DefaultSrc.Null.CSP.Policy = + 'default-src null', + 'SourceList: Null'); + + Check( + CSP.Init.DefaultSrc.Any.CSP.Policy = + 'default-src *', + 'SourceList: Any'); + + Check( + CSP.Init.DefaultSrc.Scheme('https').CSP.Policy = + 'default-src https:', + 'SourceList: Scheme'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '', '').CSP.Policy = + 'default-src example.com', + 'SourceList: Host'); + + Check( + CSP.Init.DefaultSrc.Host('https', 'example.com', '', '').CSP.Policy = + 'default-src https://example.com', + 'SourceList: Host, Scheme'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '8080', '').CSP.Policy = + 'default-src example.com:8080', + 'SourceList: Host, Port'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '', '/now').CSP.Policy = + 'default-src example.com/now', + 'SourceList: Host, Path'); + + Check( + CSP.Init.DefaultSrc.Host('', 'example.com', '8080', '/now').CSP.Policy = + 'default-src example.com:8080/now', + 'SourceList: Host, Port, Path'); + + Check( + CSP.Init.DefaultSrc.Host('https', 'example.com', '8080', '/now'). + CSP.Policy = + 'default-src https://example.com:8080/now', + 'SourceList: Scheme, Host, Port, Path'); + + Check( + CSP.Init.DefaultSrc.Host('https://example.com:8080/now').CSP.Policy = + 'default-src https://example.com:8080/now', + 'SourceList: Raw Host'); + + Check( + CSP.Init.DefaultSrc.WithSelf.CSP.Policy = + 'default-src ''self''', + 'SourceList: WithSelf'); + + Check( + CSP.Init.DefaultSrc.UnsafeInline.CSP.Policy = + 'default-src ''unsafe-inline''', + 'SourceList: UnsafeInline'); + + Check( + CSP.Init.DefaultSrc.UnsafeEval.CSP.Policy = + 'default-src ''unsafe-eval''', + 'SourceList: UnsafeEval'); + + Check( + CSP.Init.DefaultSrc.StrictDynamic.CSP.Policy = + 'default-src ''strict-dynamic''', + 'SourceList: StrictDynamic'); + + Check( + CSP.Init.DefaultSrc.UnsafeHashes.CSP.Policy = + 'default-src ''unsafe-hashes''', + 'SourceList: UnsafeHashes'); + + Check( + CSP.Init.DefaultSrc.ReportSample.CSP.Policy = + 'default-src ''report-sample''', + 'SourceList: ReportSample'); + + Check( + CSP.Init.DefaultSrc.UnsafeAllowRedirects.CSP.Policy = + 'default-src ''unsafe-allow-redirects''', + 'SourceList: UnsafeAllowRedirects'); + + Value := CSP.Init.DefaultSrc.NonceLen(Nonce).CSP.Policy; + Check( + Copy(Value, 1, Length('default-src ''nonce-')) = 'default-src ''nonce-', + 'SourceList: Nonce prefix'); + Delete(Value, 1, Length('default-src ''nonce-')); + Check( + Copy(Value, Length(Value), 1) = '''', + 'SourceList: Nonce postfix'); + Delete(Value, Length(Value), 1); + Check(Value = Nonce, 'SourceList: Nonce'); + + Value := CSP.Init.DefaultSrc.NonceLen(Nonce, 128).CSP.Policy; + Delete(Value, 1, Length('default-src ''nonce-')); + Delete(Value, Length(Value), 1); + Check(Length(Base64ToBin(Value)) = 128 shr 3, + 'SourceList: Nonce with custom length'); + + CSP.Init.DefaultSrc.NonceLen(Nonce, 4096, True).CSP.Policy; + Check((SynCommons.PosEx('-', Nonce) > 0) and + (SynCommons.PosEx('_', Nonce) > 0), + 'SourceList: Nonce in Base64url encoding'); + + Value := CSP.Init.DefaultSrc.Nonce(RawByteString(#$FF'test-nonce')). + CSP.Policy; + Check( + Copy(Value, 1, Length('default-src ''nonce-')) = 'default-src ''nonce-', + 'SourceList: Nonce by value prefix'); + Delete(Value, 1, Length('default-src ''nonce-')); + Check( + Copy(Value, Length(Value), 1) = '''', + 'SourceList: Nonce by value postfix'); + Delete(Value, Length(Value), 1); + Check(Base64ToBin(Value) = #$FF'test-nonce', 'SourceList: Nonce by value'); + + Value := CSP.Init.DefaultSrc.Nonce(RawByteString(#$FF'test-nonce'), True). + CSP.Policy; + Delete(Value, 1, Length('default-src ''nonce-')); + Delete(Value, Length(Value), 1); + Check(Base64uriToBin(Value) = #$FF'test-nonce','SourceList: Base64url Nonce'); + + Check( + CSP.Init.DefaultSrc.Nonce64('test').CSP.Policy = + 'default-src ''nonce-test''', + 'SourceList: Raw Nonce'); + + Check( + CSP.Init.DefaultSrc.SHA256('alert(''Hello, world.'');', @Hash).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256'); + Check(Hash = 'qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=', + 'SourceList: SHA256 Hash'); + + Check( + CSP.Init.DefaultSrc.SHA256('alert(''Hello, world.'');', @Hash, True). + CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng']), + 'SourceList: SHA256 in Base64url encoding'); + Check(Hash = 'qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng', + 'SourceList: SHA256 Hash in Base64url encoding'); + + Value := HexToBin( + 'ab39cb72c44ec7818008fd9d9b450228' + + '2cc21be1e267582eaba6590e86ff4e78'); + Move(Value[1], Hash256, SizeOf(THash256)); + Check( + CSP.Init.DefaultSrc.SHA256Hash(Hash256).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256Hash from THash256'); + + Value := HexToBin( + 'ab39cb72c44ec7818008fd9d9b450228' + + '2cc21be1e267582eaba6590e86ff4e78'); + Move(Value[1], Hash256, SizeOf(THash256)); + Check( + CSP.Init.DefaultSrc.SHA256Hash(Hash256, True).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng']), + 'SourceList: SHA256Hash from THash256 in Base64url encoding'); + + Value := HexToBin( + 'ab39cb72c44ec7818008fd9d9b450228' + + '2cc21be1e267582eaba6590e86ff4e78'); + Check( + CSP.Init.DefaultSrc.SHA256Hash(Value).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256Hash from RawByteString'); + + Value := HexToBin( + 'ab39cb72c44ec7818008fd9d9b450228' + + '2cc21be1e267582eaba6590e86ff4e78'); + Check( + CSP.Init.DefaultSrc.SHA256Hash(Value, True).CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng']), + 'SourceList: SHA256Hash from RawByteString in Base64url encoding'); + + Check( + CSP.Init.DefaultSrc.SHA256Hash64( + 'qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=').CSP.Policy = + FormatUTF8('default-src ''sha256-%''', + ['qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=']), + 'SourceList: SHA256Hash64'); + + Check( + CSP.Init.DefaultSrc.SHA384('alert(''Hello, world.'');', @Hash).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384'); + Check(Hash = + 'H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO', + 'SourceList: SHA384 Hash'); + + Check( + CSP.Init.DefaultSrc.SHA384('alert(''Hello, world.'');', @Hash, True). + CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t-eX6xO']), + 'SourceList: SHA384'); + Check(Hash = + 'H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t-eX6xO', + 'SourceList: SHA384 Hash in Base64url encoding'); + + Value := HexToBin( + '1fc05187c8f8f0ef6861ab5fbb9019ceae80f5120d8593b9'+ + '1f5e9d4199e02bb4fad9e9bc314b7514b9b9dadf9e5fac4e'); + Move(Value[1], Hash384, SizeOf(THash384)); + Check( + CSP.Init.DefaultSrc.SHA384Hash(Hash384).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384Hash from THash384'); + + Value := HexToBin( + '1fc05187c8f8f0ef6861ab5fbb9019ceae80f5120d8593b9'+ + '1f5e9d4199e02bb4fad9e9bc314b7514b9b9dadf9e5fac4e'); + Move(Value[1], Hash384, SizeOf(THash384)); + Check( + CSP.Init.DefaultSrc.SHA384Hash(Hash384, True).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t-eX6xO']), + 'SourceList: SHA384Hash from THash384 in Base64url encoding'); + + Value := HexToBin( + '1fc05187c8f8f0ef6861ab5fbb9019ceae80f5120d8593b9'+ + '1f5e9d4199e02bb4fad9e9bc314b7514b9b9dadf9e5fac4e'); + Check( + CSP.Init.DefaultSrc.SHA384Hash(Value).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384Hash from RawByteString'); + + Value := HexToBin( + '1fc05187c8f8f0ef6861ab5fbb9019ceae80f5120d8593b9'+ + '1f5e9d4199e02bb4fad9e9bc314b7514b9b9dadf9e5fac4e'); + Check( + CSP.Init.DefaultSrc.SHA384Hash(Value, True).CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t-eX6xO']), + 'SourceList: SHA384Hash from RawByteString in Base64url encoding'); + + Check( + CSP.Init.DefaultSrc.SHA384Hash64( + 'H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO') + .CSP.Policy = + FormatUTF8('default-src ''sha384-%''', + ['H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO']), + 'SourceList: SHA384Hash64'); + + Check( + CSP.Init.DefaultSrc.SHA512('alert(''Hello, world.'');', @Hash).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512'); + Check(Hash = + 'Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==', + 'SourceList: SHA512 Hash'); + + Check( + CSP.Init.DefaultSrc.SHA512('alert(''Hello, world.'');', @Hash, True). + CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9_A0EzCinRE7Af1ofPrw']), + 'SourceList: SHA512'); + Check(Hash = + 'Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9_A0EzCinRE7Af1ofPrw', + 'SourceList: SHA512 Hash in Base64url encoding'); + + Value := HexToBin( + '4366c54ce84400b90df213a6b3614a4c32f2edeba03f6cc56754fc2c2bd7e361' + + '69dd6a0daf76e365200778eb07adb57516ef7f0341330a29d113b01fd687cfaf'); + Move(Value[1], Hash512, SizeOf(THash512)); + Check( + CSP.Init.DefaultSrc.SHA512Hash(Hash512).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512Hash from THash512'); + + Value := HexToBin( + '4366c54ce84400b90df213a6b3614a4c32f2edeba03f6cc56754fc2c2bd7e361' + + '69dd6a0daf76e365200778eb07adb57516ef7f0341330a29d113b01fd687cfaf'); + Move(Value[1], Hash512, SizeOf(THash512)); + Check( + CSP.Init.DefaultSrc.SHA512Hash(Hash512, True).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9_A0EzCinRE7Af1ofPrw']), + 'SourceList: SHA512Hash from THash512 in Base64url encoding'); + + Value := HexToBin( + '4366c54ce84400b90df213a6b3614a4c32f2edeba03f6cc56754fc2c2bd7e361' + + '69dd6a0daf76e365200778eb07adb57516ef7f0341330a29d113b01fd687cfaf'); + Check( + CSP.Init.DefaultSrc.SHA512Hash(Value).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512Hash from RawByteString'); + + Value := HexToBin( + '4366c54ce84400b90df213a6b3614a4c32f2edeba03f6cc56754fc2c2bd7e361' + + '69dd6a0daf76e365200778eb07adb57516ef7f0341330a29d113b01fd687cfaf'); + Check( + CSP.Init.DefaultSrc.SHA512Hash(Value, True).CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9_A0EzCinRE7Af1ofPrw']), + 'SourceList: SHA512Hash from RawByteString in Base64url encoding'); + + Check( + CSP.Init.DefaultSrc.SHA512Hash64( + 'Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==').CSP.Policy = + FormatUTF8('default-src ''sha512-%''', + ['Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp' + + '3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==']), + 'SourceList: SHA512Hash64'); +end; + { TBoilerplateHTTPServerSteps } procedure TBoilerplateHTTPServerSteps.GivenOptions( @@ -2200,11 +3575,17 @@ procedure TBoilerplateHTTPServerSteps.GivenClearServer; end; procedure TBoilerplateHTTPServerSteps.GivenContentSecurityPolicy( - const Value: RawUTF8); + const Value: SockString); begin ContentSecurityPolicy := Value; end; +procedure TBoilerplateHTTPServerSteps.GivenContentSecurityPolicyReportOnly( + const Value: SockString); +begin + ContentSecurityPolicyReportOnly := Value; +end; + procedure TBoilerplateHTTPServerSteps.GivenModifiedFile( const FileName: TFileName; const KeepTimeStamp, KeepSize: Boolean); @@ -2483,11 +3864,6 @@ procedure TBoilerplateApplication._404( Scope.Content := 'NOT FOUND'; end; -procedure TBoilerplateFeatures.Scenarios; -begin - AddCase(TBoilerplateHTTPServerShould); -end; - { T404Application } procedure T404Application.Default(var Scope: Variant); @@ -2512,4 +3888,11 @@ procedure T404Application._404(const Dummy: Integer; out Scope: Variant); Is404Called := True; end; +procedure TBoilerplateFeatures.Scenarios; +begin + AddCase(TBoilerplateHTTPServerShould); + AddCase(TCSP2Should); + AddCase(TCSP3Should); +end; + end. diff --git a/Tests/mORMotBPTests.dpr b/Tests/mORMotBPTests.dpr index 02ca1d8..353c44b 100644 --- a/Tests/mORMotBPTests.dpr +++ b/Tests/mORMotBPTests.dpr @@ -5,8 +5,8 @@ Before running the tests you must build Assets.res file To do this add the next two events to the "Pre-build events" project options: -"$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\Assets" "$(PROJECTDIR)\assets.tmp" -"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\assets.tmp" +"$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\Assets" "$(PROJECTDIR)\Assets.tmp" +"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\Assets.tmp" For Delphi 2007 IDE and above: @@ -44,6 +44,7 @@ uses {$I SynDprUses.inc} // will enable FastMM4 prior to Delphi 2006, and enable FPC on linux BoilerplateAssets in '../BoilerplateAssets.pas', BoilerplateHTTPServer in '../BoilerplateHTTPServer.pas', + CSP in '../CSP.pas', BoilerplateTests in 'BoilerplateTests.pas'; begin diff --git a/Tests/mORMotBPTests.ini b/Tests/mORMotBPTests.ini index 7e610cb..88b0e42 100644 --- a/Tests/mORMotBPTests.ini +++ b/Tests/mORMotBPTests.ini @@ -1,5 +1,5 @@ [Build Events] -PreBuild=""$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\Assets" "$(PROJECTDIR)\assets.tmp"|"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\assets.tmp"" +PreBuild=""$(PROJECTDIR)\..\Tools\assetslz" -GZ1 -B1 "$(PROJECTDIR)\Assets" "$(PROJECTDIR)\Assets.tmp"|"$(PROJECTDIR)\..\Tools\resedit" -D "$(PROJECTDIR)\Assets.res" rcdata ASSETS "$(PROJECTDIR)\Assets.tmp"" PostBuild="" BuildEvent=3 PreBuildEnabled=1 diff --git a/Tools/resedit.dpr b/Tools/resedit.dpr index 9a543c6..af69827 100644 --- a/Tools/resedit.dpr +++ b/Tools/resedit.dpr @@ -547,8 +547,6 @@ end; {$ENDIF} begin - {$IFDEF DEBUG} ReportMemoryLeaksOnShutdown := True; {$ENDIF} - try if not ParseParameters then begin