Thin Hook Preprocessor (experimental)
- [Vulnerability Fix] Since 0.4.0-alpha.45 with Fix #398 Unchain policy objects to
Object.prototype
, access policy objects are immune to contaminatedObject.prototype
properties whentargetConfig.policy.unchainAcl = true
is configured (which is disabled by default for compatibility). Prior to this version, writableObject.prototype
properties can contaminate access policy objects. - [Vulnerability Fix] Since 0.4.0-alpha.45 with Fix #399 Recognize acl for
Object.prototype
object properly,acl.Object[S_PROTOTYPE]
policies forObject.prototype
properties are properly applied whentargetConfig.policy.unchainAcl = true
is configured (which is disabled by default for compatibility). Prior to this version,acl.Object
policies are incorrectly applied forObject.prototype
properties in some cases. - [Enhancement] Since 0.4.0-alpha.28 with Issue #376 Support ES modules, ACL policies are applied to ES modules by hooking ES module objects. This feature is optional and can be disabled by
hook.parameters.importMapper = null
indemo/bootstrap.js
- [Vulnerability Fix] Since 0.4.0-alpha.25 with Fix #369 Block DOM intrusion by Browser Extensions, the application hangs up with an alert message on DOM intrusion by Browser Extensions. Prior to this version, Browser Extensions can intrude into DOM and manipulate contents.
- [Vulnerability Fix] Since 0.4.0-alpha.24 with Fix #368 Check Service Worker cache integrity, integrity of Service Worker cache contents is verified with HMAC keys. Prior to this version, corrupted Service Worker cache contents can intrude into the application.
- [Vulnerability Fix] Since 0.4.0-alpha.22 with Fix #363 Block blob URLs, blob URLs are blocked except for
<a download="filename" href="blob:...">Download Link</a>
. Prior to this version, documents with blob URLs bypass Service Worker. - [Vulnerability Fix] Since 0.4.0-alpha.22 with Fix #362 Option to block
<embed>
and<object>
elements, the application hangs up on<embed>
and<object>
activities withhook.parameters.hangUpOnEmbedAndObjectElement = true
. Prior to this version,<embed>
and<object>
documents can bypass Service Worker with Chrome Canary 86. - [Vulnerability Fix] Since 0.4.0-alpha.21 with Fix #355 Treat proxy objects as alias objects in ACL, ACL is properly applied for proxy objects created via
new Proxy(target, handler)
andProxy.revocable(target, handler)
as with their originaltarget
objects. Prior to this version, ACL for thetarget
objects are not applied to proxy objects. - [Vulnerability Fix] Since 0.4.0-alpha.20 with Fix #350 Append the target value to hooked arguments and pick it up in
Policy.defaultAcl()
, ACL is properly applied forwith
-scoped values in function calls and constructor calls. Prior to this version, calls towith
-scoped functions can skip ACLs for their reference values. - [Vulnerability Fix] Since 0.4.0-alpha.20 with Fix #349 Hook local function calls in
with
clause, local function calls inwith
clause are property hooked. Prior to this version, local function calls inwith
clause are not hooked. This is a regression issue from the fix for Fix #339 Local variables in a with block are mistreated as global variables in ACL #339. - [Vulnerability Fix] Since 0.4.0-alpha.20 with Fix #348 Hook tagged template literals as function calls, tag functions of tagged template literals are hooked as function calls. Prior to this version, calls to tag functions of tagged template literals are not hooked.
- [Vulnerability Fix] Since 0.4.0-alpha.19 with Fix #347 Apply ACL for properties of primitive values,
acl.type[S_PROTOTYPE][S_INSTANCE]
ACLs for primitive type classesString
,Number
,Boolean
,Symbol
, andBigInt
are applied to properties of primitive values. Prior to this version, ACL is not applied to properties of primitive values. - [Vulnerability Fix] Since 0.4.0-alpha.17 with Fix #344 Normalize
with
namespace objects before bound function detection, bound function calls in awith
clause is properly detected. Prior to this version, ACL for unbound original function is not applied for bound function calls in awith
clause. - [Vulnerability Fix] Since 0.4.0-alpha.16 with Fix #342 Chain
acl[S_DEFAULT][S_PROTOTYPE][S_INSTANCE]
toacl.Object[S_PROTOTYPE][S_INSTANCE]
,acl.Object[S_PROTOTYPE][S_INSTANCE]
is applied for anonymous object properties. Prior to this version,acl.Object[S_PROTOTYPE][S_INSTANCE]
is not applied anonymous object properties. - [Vulnerability Fix] Since 0.4.0-alpha.15 with Fix #341 Apply ACL to all sources of Object.assign(), ACL is applied for all sources of
Object.assign()
even they contain falsy values. Prior to this version, ACL is not applied for sources ofObject.assign()
if the first source is not an object likeundefined
. - [Vulnerability Fix] Since 0.4.0-alpha.13 with Fix #336 Apply ACL for super classes/objects of global classes/objects with no dedicated ACLs, ACL is applied for super classes/objects of global classes/objects and instances of global classes even without dedicated ACLs. Prior to this version, ACL is not applied for super classes/objects of global classes/objects and instances of global classes without dedicated ACLs.
- [Vulnerability Fix] Since 0.4.0-alpha.9 with Fix #336 Apply ACL for super classes/objects, ACL is applied for super classes/objects of non-global classes/objects and instances of non-global classes. Prior to this version, ACL is not applied for super classes/objects of non-global classes/objects and instances of non-global classes.
- [Vulnerability Fix] Since 0.4.0-alpha.8 with Fix #334 Apply ACL for reading the target property of
receiver
inReflect.get()
, ACL is applied forreceiver
inReflect.get()
to read the target property. Prior to this version, ACL is not applied forreceiver
inReflect.get()
. - [Vulnerability Fix] Since 0.4.0-alpha.7 with Fix #333 Check constructors of Object.assign() source objects, ACL is applied for class instances as source objects in
Object.assign()
by checking their constructors. Prior to this version, ACL for class instances as source objects inObject.assign()
is not applied while ACL for global objects is applied properly. This fix is to supplement the fix for Fix #324 Apply ACL for S_TARGETED normalized properties with S_ALL normalized property. - [Vulnerability Fix] Since 0.4.0-alpha.6 with Fix #331 Check for all property access of destructured argument objects, destructured argument objects of functions are checked against all property access when called. Prior to this version, all properties of destructured argument objects of functions can be read without S_ALL access.
- [Vulnerability Fix] Since 0.4.0-alpha.5 with Fix #327 Hook RHS values of ObjectPattern and ArrayPattern for checking all property access, RHS values of ObjectPattern and ArrayPattern are hooked for checking all property access of the target values. Prior to this version, all properties of RHS values in ObjectPattern and ArrayPattern can be iterated without S_ALL access.
- [Vulnerability Fix] Since 0.4.0-alpha.4 with Fix #325 Track global objects set via defineProperty(), etc., global objects set via
Object.defineProperty()
, etc. are properly tracked for ACL. Prior to this version, ACL is skipped for global objects set viaObject.defineProperty()
, etc. - [Vulnerability Fix] Since 0.4.0-alpha.3 with Fix #324 Apply ACL for S_TARGETED normalized properties with S_ALL normalized property, source objects in
Object.assign()
are checked against ACL for reading all properties. Prior to this version, ACL is skipped for source objects inObject.assign()
and any enumerable properties in the source objects can be assigned to the target object. - [Vulnerability Fix] Since 0.4.0-alpha.2 with Fix #310 Hang up with an infinite loop if a hacked Service Worker tries to replace the entry page, the entry page hangs up if a hacked Service Worker tries to navigate the entry page. Prior to this version, a hacked Service Worker from an MITM attacker can replace the entry page with an arbitrary URL.
- [Vulnerability Fix] Since 0.3.7 with Fix #319 Check HTML/SVG extensions and content-type case-insensitively, extensions and content-type for HTML and SVG are checked case-insensitively. Prior to this version, extensions and content-type for HTML and SVG are check case-sensitively and some contents with capital letters such as *.HTML with content-type TEXT/HTML are not detected as HTML and can bypass hooking.
- [Vulnerability Fix] Since 0.3.6 with Fix #318 Check Worker extensions, extensions for Worker URLs are checked against .m?js. Prior to this version, extensions for Worker URLs are not checked and workers with irregular extensions can bypass hooking.
- [Vulnerability Fix] Since 0.3.5 with Fix #316 Redirect top SVG to about:blank, top SVG document is redirected to about:blank. Prior to this version, top SVG document can invalidate disable-devtools.js and DevTools is unexpectedly enabled.
- [Vulnerability Fix] Since 0.3.4 with Fix #314 Check content-type for HTML and SVG as well as extensions, content-type is checked for HTML/SVG detection as well as extensions. Prior to this version, HTML/SVG responses with irregular extensions are not detected as HTML/SVG and thus not hooked.
- [Vulnerability Fix] Since 0.3.3 with Fix #313 GET errorReport.json with 307 about:blank response, 307 redirect to
about:blank
is responded for GET errorReport.json request. Prior to this version, 404 Not Found is responded for GET errorReport.json, whose HTML contents in iframe can be accessed bypassing access policies. - [Feature Enhancements] Since 0.3.0 with Fix #284 Additional Content-Types in cache-bundle.json, extended metadata are supported in
cache-bundle.json
to add additional cacheable content types. This README document is updated to describe the new features and their configurations. - [Feature Enhancements] Since 0.2.0 with Fix #266 Block access via automation like puppeteer, there are many significant changes on global object access and hooking. ACL is basically compatible with prior versions but extra configurations for new features are required. This README document is updated to describe the new features and their configurations.
- [Vulnerability Fix] Since 0.1.11 with Fix #265 Attach context to wrapper property name for global object access, the correct contexts are used for global object access in self-assignment. Prior to this version, the context for the RHS value in self-assignment is incorrectly used for the access to the object.
- [Vulnerability Fix] Since 0.1.9 with Fix #263 Use the current context for global object access, the correct contexts are used for global object read/write/call access. Prior to this version, the context for the first access to the target global object is incorrectly used for the following access to the object.
- [Configuration] Since 0.0.250 with Fix #252 Block direct access to source codes and Fix #254 Block direct access to source codes even after the app shutdown, direct access to source codes are blocked.
hook.parameters.appPathRoot = '/';
indemo/disable-devtools.js
can be configured to set the root of the application assets. Prior to this version, direct access to source codes are allowed. - [Vulnerability Fix] Since 0.0.243 with Fix #250 Hook scripts in SVG and block data:/blob: URLs for SVG, scripts in SVG are hooked and
blob:
anddata:
URLs are blocked for SVG. Prior to this version, scripts in SVG are not hoooked andblob:
anddata:
URLs are allowed for SVG.<object data="inline-script.svg"></object>
,<embed src="inline-script.svg">
,<iframe src="inline-script.svg"></iframe>
- [Vulnerability Fix] Since 0.0.239 with Fix #249 Block blob: URLs for Worker,
blob:
anddata:
URLs are blocked forWorker
andSharedWorker
. Prior to this version,blob:
anddata:
URLs are allowed forWorker
andSharedWorker
. - [Vulnerability Fix] Since 0.0.236 with Fix #247 Hook script.text property, script.text property is properly hooked. Prior to this version, script.text property is not hooked.
- [Vulnerability Fix] Since 0.0.236 with Fix #246 Handle non-http protocols in iframe.src, script.src properly, non-http protocols in iframe src and script src are handled properly. Prior to this version, non-http protocols in iframe src and script src are not handled properly.
- [Vulnerability Fix] Since 0.0.235 with Fix #245 no-hook-authorization parameter is missing in sub documents, unauthorized no hook scripts are blocked in sub documents. Prior to this version, unauthorized no hook scripts in sub documents are not blocked.
- [Vulnerability Fix] Since 0.0.233 with Fix #242 Hook iframe.srcdoc,
iframe.srcdoc
is hooked asonload
attribute. Prior to this version,iframe.srcdoc
is not hooked. - [Vulnerability Fix] Since 0.0.232 with Fix #241 AsyncFunction() is not hooked,
AsyncFunction('script')
is properly hooked. Prior to this version,AsyncFunction('script')
is not hooked.AsyncFunction = (async function() {}).constructor
- [Vulnerability Fix] Since 0.0.231 with Fix #240 object.Function() is not hooked,
object.Function('script')
is properly hooked. Prior to this version,object.Function('script')
is not hooked. - [Vulnerability Fix] Since 0.0.230 with Fix #239 Full ACLs for iframe.contentWindow, full ACLs for iframe.contentWindow are properly applied. Prior to this version, only partial ACLs for iframe.contentWindow are applied.
- [Vulnerability Fix] Since 0.0.229 with Fix #238 No ACLs for iframe.contentWindow, global object ACLs for iframe.contentWindow are properly applied. Prior to this version, global object ACLs for iframe.contentWindow are not applied.
- [Vulnerability Fix] Since 0.0.228 with Fix #234 Global ACLs are not applied in web workers, ACLs for global objects in web workers are properly applied. Prior to this version, ACLs for global objects in web workers are not applied.
- [Performance Optimization]
__hook__acl
indemo/hook-callback.js
should be used as it is much faster than__hook__
as described in Fix #230. Modification:Object.defineProperty(_global, '__hook__', { configurable: false, enumerable: false, writable: false, value: hookCallbacks.__hook__acl });
- [ACL Compatibility] Since 0.0.225 with Fix #229 Exclude Multiple ACLs for global object properties, ACLs for the global object properties (
top
,parent
,frames
,global
,_global
, etc.) other than the main global object property (window
in the main document,self
in workers) are applied only for access likewindow.top
. In 0.0.224, all the ACLs for the global object properties are applied for every global object access, which is redundant. - [Vulnerability Fix] Since 0.0.225 with Fix #227 Private API registered in strict mode, ACLs for private APIs registered to the global object in strict mode are properly applied. Prior to this version, ACLs for private APIs registered to the global object in strict mode are not applied.
- [ACL Compatibility] Since 0.0.225 with Fix #226 Multiple ACLs,
_globalObjects
is aSetMap
object defined inhook-callback.js
and_globalObjects.get(obj)
return aSet
object containingstring
s. All the ACLs for the set ofstring
s are applied for the object. Prior to this version,_globalObjects
is aMap
object and_globalObjects.get(obj)
returns astring
. - [ACL Compatibility] Since 0.0.225 with Fix #226 Multiple ACLs,
_blacklistObjects
is deprecated. - [ACL Compatibility] Since 0.0.216 with Fix #217,
delete
operations require'W'
permission as they can delete properties with customized descriptors. Prior to this version,delete
operations require'w'
permission. - [ACL Compatibility] Since 0.0.214 with Fix #215,
'R'
and'W'
opTypes are introduced for getting/setting property descriptors, i.e., contexts to access descriptors must have explicit'R'
and/or'W'
permissions for the target properties. Prior to 0.0.213, property descriptors can be accessed by mere'r'
and/or'w'
permissions. - [Vulnerability Fix] Since 0.0.211 with Fix #211, bypassing of ACL for global objects by dummy custom element definition is avoided. Prior to this version, ACL can be skipped by defining dummy custom elements by standard elements as constructor classes.
- [Vulnerability Fix] Since 0.0.209 with Fix #210, bypassing of ACL for global objects by cloing them to other global objects is avoided. Prior to this version, ACL can be skipped by cloing global objects.
- [Vulnerability Fix] Since 0.0.205 with Fix #208, scripts via
document.writeln()
are hooked as indocument.write()
. Prior to this version, scripts viadocument.writeln()
are not hooked. - [Vulnerability Fix] Since 0.0.203 with Fix #207,
textContent
ofscript
elements are always treated as JavaScript scripts regardless of their configured MIME types (type
property/attribute). Prior to this version,textContent
ofscript
elements containing__hook__
as strings can be mistaken as HOOKED scripts and run without hooking. - [Context Generator Compatibility] Since 0.0.148 with #144, the old context generator
"method"
is renamed as"oldMethod"
and the"cachedMethod"
is renamed as"method"
and become the new default context generator. The"cachedMethod"
remains as an alias for the new"method"
context generator. There are slight changes in the new"method"
context generator. A warning message is shown on the debug console to notify the change.
old name | new name | feature |
---|---|---|
method |
oldMethod |
script.js,Class,method |
cachedMethod |
method |
script.js,Class,method including computed property names |
- [Hook Callback Compatibility] Since 0.0.149 with #123, the hook callback function has to support new operators for hooking in strict mode. See below for the updated hook callback function
hook.__hook__
.hook.hookCallbackCompatibilityTest()
can detect if the target hook callback function is compatible or not. - [Opaque URL Authorization] Since 0.0.178 with #178, all opaque content URLs must be authorized via
hook.parameters.opaque = [ 'opaque_url', ..., (url) => url.match(/opaque_url_pattern/), ... ]
configuration.
Demo on GitHub Pages
class C {
add(a = 1, b = 2) {
let plus = (x, y) => x + y;
return plus(a, b);
}
}
const __context_mapper__ = $hook$.$(__hook__, [
'examples/example2.js,C',
'_p_C;examples/example2.js,C',
'examples/example2.js,C,add',
'examples/example2.js,C,add,plus'
]);
$hook$.global(__hook__, __context_mapper__[0], 'C', 'class')[__context_mapper__[1]] = class C {
add(a, b) {
return __hook__((a = 1, b = 2) => {
let plus = (...args) => __hook__((x, y) => x + y, null, args, __context_mapper__[3]);
return __hook__(plus, null, [
a,
b
], __context_mapper__[2], 0);
}, null, arguments, __context_mapper__[2]);
}
};
const hook = require('thin-hook/hook.js');
let code = fs.readFileSync('src/target.js', 'UTF-8');
let initialContext = [['src/target.js', {}]];
let gen = hook(code, '__hook__', initialContext, 'hash');
fs.writeFileSync('hooked/target.js', gen);
fs.writeFileSync('hooked/target.js.contexts.json', JSON.stringify(contexts, null, 2));
// Built-in Context Generator Function
hook.contextGenerators.method = function generateMethodContext(astPath) {
return astPath.map(([ path, node ], index) => node && node.type
? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
: index === 0 ? path : '').filter(p => p).join(',');
}
// Example Custom Context Generator Function with Hashing
const hashSalt = '__hash_salt__';
let contexts = {};
hook.contextGenerators.hash = function generateHashContext(astPath) {
const hash = hook.utils.createHash('sha256');
let hashedInitialContext = astPath[0][0];
astPath[0][0] = contexts[hashedInitialContext] || astPath[0][0];
let methodContext = hook.contextGenerators.method(astPath);
astPath[0][0] = hashedInitialContext;
hash.update(hashSalt + methodContext);
let hashContext = hash.digest('hex');
contexts[hashContext] = methodContext;
return hashContext;
}
{
// Authorization Tickets for no-hook scripts
// Ticket for this script itself is specified in URL of script tag as
// hook.min.js?no-hook-authorization={ticket}
// Note: no-hook-authorization must not exist in learning mode
let noHookAuthorization = {
// '*' is for learning mode to detect authorization tickets in
// hook.parameters.noHookAuthorizationPassed,
// hook.parameters.noHookAuthorizationFailed
// JSONs are output to console in the learning mode
//'*': true,
"35ae97a3305b863af7eb0ac75c8679233a2a7550e4c3046507fc9ea182c03615": true,
"16afd3d5aa90cbd026eabcc4f09b1e4207a7042bc1e9be3b36d94415513683ed": true,
"ae11a06c0ddec9f5b75de82a40745d6d1f92aea1459e8680171c405a5497d1c8": true,
"5b7ebf7b0b2977d44f47ffa4b19907abbc443feb31c343a6cbbbb033c8deb01a": true,
"c714633723320be54f106de0c50933c0aeda8ac3fba7c41c97a815ed0e71594c": true,
"2f43d927664bdfcbcb2cc4e3743652c7eb070057efe7eaf43910426c6eae7e45": true,
"b397e7c81cca74075d2934070cbbe58f345d3c00ff0bc04dc30b5c67715a572f": true,
"02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a": true,
"aebb23ce36eb6f7d597d37727b4e6ee5a57aafc564af2d65309a9597bfd86625": true
};
let hidden;
const passcode = 'XX02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a';
if (typeof self === 'object' && self.constructor.name === 'ServiceWorkerGlobalScope') {
// Service Worker
let reconfigure = false;
if (hook.parameters.noHookAuthorization) {
if (Object.getOwnPropertyDescriptor(hook.parameters, 'noHookAuthorization').configurable) {
reconfigure = true;
}
}
else {
reconfigure = true;
}
if (reconfigure) {
Object.defineProperty(hook.parameters, 'noHookAuthorization', {
configurable: false,
enumerable: true,
get() {
return hidden;
},
set(value) {
if (value && value.passcode === passcode) {
delete value.passcode;
Object.freeze(value);
hidden = value;
}
}
});
}
noHookAuthorization.passcode = passcode;
hook.parameters.noHookAuthorization = noHookAuthorization;
}
else {
// Browser Document
Object.defineProperty(hook.parameters, 'noHookAuthorization', {
configurable: false,
enumerable: true,
writable: false,
value: Object.freeze(noHookAuthorization)
});
}
if (!noHookAuthorization['*']) {
Object.seal(hook.parameters.noHookAuthorizationPassed);
}
}
{
// source map target filters
hook.parameters.sourceMap = [
url => location.origin === url.origin && url.pathname.match(/^\/components\/thin-hook\/demo\//)
];
// hook worker script URL
hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
}
// Hook worker script (demo/hook-worker.js)
//
// Configuration:
// hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
importScripts('../hook.min.js?no-hook=true', 'context-generator.js?no-hook=true', 'bootstrap.js?no-hook=true');
onmessage = hook.hookWorkerHandler;
<!-- Example Custom Context Generator for Service Worker and Browser Document -->
<script src="bower_components/thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&context-generator-name=method2&fallback-page=index-fb.html&service-worker-ready=true"></script>
<script context-generator src="custom-context-generator.js?no-hook=true"></script>
<script context-generator no-hook>
{
hook.contextGenerators.method2 = function generateMethodContext2(astPath) {
return astPath.map(([ path, node ], index) => node && node.type
? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
: index === 0 ? path : '').filter(p => p).join(',') +
(astPath[astPath.length - 1][1].range ? ':' + astPath[astPath.length - 1][1].range[0] + '-' + astPath[astPath.length - 1][1].range[1] : '');
}
Object.freeze(hook.contextGenerators);
// CORS script list
hook.parameters.cors = [
'https://raw.githubusercontent.com/t2ym/thin-hook/master/examples/example1.js',
(url) => { let _url = new URL(url); return _url.hostname !== location.hostname && ['www.gstatic.com'].indexOf(_url.hostname) < 0; }
];
// Authorized opaque URL list
hook.parameters.opaque = [
'https://www.gstatic.com/charts/loader.js',
(url) => {
let _url = new URL(url);
return _url.hostname !== location.hostname &&
_url.href.match(/^(https:\/\/www.gstatic.com|https:\/\/apis.google.com\/js\/api.js|https:\/\/apis.google.com\/_\/)/);
}
];
}
</script>
// Built-in Minimal Hook Callback Function without hooking properties (hook-property=false)
hook.__hook_except_properties__ = function __hook_except_properties__(f, thisArg, args, context, newTarget) {
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
// the global object
const _global = (new Function('return this'))();
// helper for strict mode
class StrictModeWrapper {
static ['#.'](o, p) { return o[p]; }
static ['#[]'](o, p) { return o[p]; }
static ['#*'](o) { return o; }
static ['#in'](o, p) { return p in o; }
static ['#()'](o, p, a) { return o[p](...a); }
static ['#p++'](o, p) { return o[p]++; }
static ['#++p'](o, p) { return ++o[p]; }
static ['#p--'](o, p) { return o[p]--; }
static ['#--p'](o, p) { return --o[p]; }
static ['#delete'](o, p) { return delete o[p]; }
static ['#='](o, p, v) { return o[p] = v; }
static ['#+='](o, p, v) { return o[p] += v; }
static ['#-='](o, p, v) { return o[p] -= v; }
static ['#*='](o, p, v) { return o[p] *= v; }
static ['#/='](o, p, v) { return o[p] /= v; }
static ['#%='](o, p, v) { return o[p] %= v; }
static ['#**='](o, p, v) { return o[p] **= v; }
static ['#<<='](o, p, v) { return o[p] <<= v; }
static ['#>>='](o, p, v) { return o[p] >>= v; }
static ['#>>>='](o, p, v) { return o[p] >>>= v; }
static ['#&='](o, p, v) { return o[p] &= v; }
static ['#^='](o, p, v) { return o[p] ^= v; }
static ['#|='](o, p, v) { return o[p] |= v; }
static ['#.='](o, p) { return { set ['='](v) { o[p] = v; }, get ['=']() { return o[p]; } }; }
}
// Built-in Minimal Hook Callback Function with hooking properties (hook-property=true) - default
function __hook__(f, thisArg, args, context, newTarget) {
let normalizedThisArg = thisArg;
if (newTarget === false) { // resolve the scope in 'with' statement body
let varName = args[0];
let __with__ = thisArg;
let scope = _global;
let _scope;
let i;
for (i = 0; i < __with__.length; i++) {
_scope = __with__[i];
if (Reflect.has(_scope, varName)) {
if (_scope[Symbol.unscopables] && _scope[Symbol.unscopables][varName]) {
continue;
}
else {
scope = _scope;
break;
}
}
}
thisArg = normalizedThisArg = scope;
}
let result;
let args1 = args[1]; // for '()'
function * gen() {}
let GeneratorFunction = gen.constructor;
switch (f) {
case Function:
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args);
break;
case GeneratorFunction:
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, true);
break;
case '()':
case '#()':
switch (thisArg) {
case Reflect:
switch (args[0]) {
case 'construct':
if (args[1]) {
switch (args[1][0]) {
case Function:
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
if (args[1][2]) {
args1.push(args[1][2]);
}
break;
default:
if (args[1][0].prototype instanceof Function) {
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], args[1][0].prototype instanceof GeneratorFunction)];
if (args[1][2]) {
args1.push(args[1][2]);
}
}
break;
}
}
break;
case 'apply':
if (args[1]) {
switch (args[1][0]) {
case Function:
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2])];
break;
case GeneratorFunction:
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], true)];
break;
default:
if (args[1][0].prototype instanceof Function) {
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], args[1][0].prototype instanceof GeneratorFunction)];
}
break;
}
}
break;
default:
break;
}
break;
case Function:
switch (args[0]) {
case 'apply':
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
break;
case 'call':
args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1))];
break;
default:
break;
}
break;
case GeneratorFunction:
switch (args[0]) {
case 'apply':
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], true)];
break;
case 'call':
args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1), true)];
break;
default:
break;
}
break;
default:
if (thisArg instanceof GeneratorFunction && args[0] === 'constructor') {
args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1], true);
}
else if (thisArg instanceof Function && args[0] === 'constructor') {
args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1]);
}
break;
}
break;
default:
if (typeof f === 'function') {
if (f.prototype instanceof Function && newTarget) {
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, f.prototype instanceof GeneratorFunction);
}
else if (newTarget === '') {
if (args[0] && Object.getPrototypeOf(args[0]) === Function) {
args = [ args[0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args.slice(1)) ];
}
}
}
break;
}
if (typeof f !== 'string') {
result = newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
else {
// property access
switch (f) {
// getter
case '.':
case '[]':
result = thisArg[args[0]];
break;
// enumeration
case '*':
result = thisArg;
break;
// property existence
case 'in':
result = args[0] in thisArg;
break;
// funcation call
case '()':
result = thisArg[args[0]](...args1);
break;
// unary operators
case 'p++':
result = thisArg[args[0]]++;
break;
case '++p':
result = ++thisArg[args[0]];
break;
case 'p--':
result = thisArg[args[0]]--;
break;
case '--p':
result = --thisArg[args[0]];
break;
case 'delete':
result = delete thisArg[args[0]];
break;
// assignment operators
case '=':
result = thisArg[args[0]] = args[1];
break;
case '+=':
result = thisArg[args[0]] += args[1];
break;
case '-=':
result = thisArg[args[0]] -= args[1];
break;
case '*=':
result = thisArg[args[0]] *= args[1];
break;
case '/=':
result = thisArg[args[0]] /= args[1];
break;
case '%=':
result = thisArg[args[0]] %= args[1];
break;
case '**=':
result = thisArg[args[0]] **= args[1];
break;
case '<<=':
result = thisArg[args[0]] <<= args[1];
break;
case '>>=':
result = thisArg[args[0]] >>= args[1];
break;
case '>>>=':
result = thisArg[args[0]] >>>= args[1];
break;
case '&=':
result = thisArg[args[0]] &= args[1];
break;
case '^=':
result = thisArg[args[0]] ^= args[1];
break;
case '|=':
result = thisArg[args[0]] |= args[1];
break;
// LHS property access
case '.=':
result = { set ['='](v) { thisArg[args[0]] = v; }, get ['=']() { return thisArg[args[0]]; } };
break;
// strict mode operators prefixed with '#'
// getter
case '#.':
case '#[]':
result = StrictModeWrapper['#.'](thisArg, args[0]);
break;
// enumeration
case '#*':
result = StrictModeWrapper['#*'](thisArg);
break;
// property existence
case '#in':
result = StrictModeWrapper['#in'](thisArg, args[0]);
break;
// funcation call
case '#()':
result = StrictModeWrapper['#()'](thisArg, args[0], args1);
break;
// unary operators
case '#p++':
result = StrictModeWrapper['#p++'](thisArg, args[0]);
break;
case '#++p':
result = StrictModeWrapper['#++p'](thisArg, args[0]);
break;
case '#p--':
result = StrictModeWrapper['#p--'](thisArg, args[0]);
break;
case '#--p':
result = StrictModeWrapper['#--p'](thisArg, args[0]);
break;
case '#delete':
result = StrictModeWrapper['#delete'](thisArg, args[0]);
break;
// assignment operators
case '#=':
result = StrictModeWrapper['#='](thisArg, args[0], args[1]);
break;
case '#+=':
result = StrictModeWrapper['#+='](thisArg, args[0], args[1]);
break;
case '#-=':
result = StrictModeWrapper['#-='](thisArg, args[0], args[1]);
break;
case '#*=':
result = StrictModeWrapper['#*='](thisArg, args[0], args[1]);
break;
case '#/=':
result = StrictModeWrapper['#/='](thisArg, args[0], args[1]);
break;
case '#%=':
result = StrictModeWrapper['#%='](thisArg, args[0], args[1]);
break;
case '#**=':
result = StrictModeWrapper['#**='](thisArg, args[0], args[1]);
break;
case '#<<=':
result = StrictModeWrapper['#<<='](thisArg, args[0], args[1]);
break;
case '#>>=':
result = StrictModeWrapper['#>>='](thisArg, args[0], args[1]);
break;
case '#>>>=':
result = StrictModeWrapper['#>>>='](thisArg, args[0], args[1]);
break;
case '#&=':
result = StrictModeWrapper['#&='](thisArg, args[0], args[1]);
break;
case '#^=':
result = StrictModeWrapper['#^='](thisArg, args[0], args[1]);
break;
case '#|=':
result = StrictModeWrapper['#|='](thisArg, args[0], args[1]);
break;
// LHS property access
case '#.=':
result = StrictModeWrapper['#.='](thisArg, args[0]);
break;
// getter for super
case 's.':
case 's[]':
result = args[1](args[0]);
break;
// super method call
case 's()':
result = args[2](args[0]).apply(thisArg, args[1]);
break;
// unary operators for super
case 's++':
case '++s':
case 's--':
case '--s':
result = args[1].apply(thisArg, args);
break;
// assignment operators for super
case 's=':
case 's+=':
case 's-=':
case 's*=':
case 's/=':
case 's%=':
case 's**=':
case 's<<=':
case 's>>=':
case 's>>>=':
case 's&=':
case 's^=':
case 's|=':
result = args[2].apply(thisArg, args);
break;
// getter in 'with' statement body
case 'w.':
case 'w[]':
result = args[1]();
break;
// function call in 'with' statement body
case 'w()':
result = args[2](...args[1]);
break;
// constructor call in 'with' statement body
case 'wnew':
result = args[2](...args[1]);
break;
// unary operators in 'with' statement body
case 'w++':
case '++w':
case 'w--':
case '--w':
result = args[1]();
break;
// unary operators in 'with' statement body
case 'wtypeof':
case 'wdelete':
result = args[1]();
break;
// LHS value in 'with' statement body (__hook__('w.=', __with__, ['p', { set ['='](v) { p = v } } ], 'context', false)['='])
case 'w.=':
result = args[1];
break;
// assignment operators in 'with' statement body
case 'w=':
case 'w+=':
case 'w-=':
case 'w*=':
case 'w/=':
case 'w%=':
case 'w**=':
case 'w<<=':
case 'w>>=':
case 'w>>>=':
case 'w&=':
case 'w^=':
case 'w|=':
result = args[2](args[1]);
break;
// default (invalid operator)
default:
f(); // throw TypeError: f is not a function
result = null;
break;
}
}
return result;
}
// Example Hook Callback Function with Primitive Access Control
hashContext = { 'hash': 'context', ... }; // Generated from hook.preprocess initialContext[0][1]
trustedContext = { 'context': /trustedModules/, ... }; // Access Policies
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
console.log('hook:', context, args);
if (!hashContext[context] ||
!trustedContext[hashContext[context]] ||
!(new Error('').stack.match(trustedContext[hashContext[context]]))) {
// plus check thisArg, args, etc.
throw new Error('Permission Denied');
}
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
If hooking is performed run-time in Service Worker, the entry HTML page must be loaded via Service Worker so that no hook-targeted scripts are evaluated without hooking.
To achieve this, the static entry HTML has to be Encoded at build time by hook.serviceWorkerTransformers.encodeHTML(html)
.
# encode src/index.html to dist/index.html
hook --out dist/index.html src/index.html
<html>
<head>
<script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=true"></script>
<!-- Hook Callback Function witout hooking properties -->
<script no-hook>
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
...
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
</script><!-- end of mandatory no-hook scripts -->
<!-- comment --->
<script src="..."></script>
...
</html>
<html>
<head>
<script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=false"></script></head></html>
<!-- Hook Callback Function without hooking properties -->
<script no-hook>
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
...
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
</script><!--<C!-- end of mandatory no-hook scripts --C>
<C!-- comment --C>
<script src="..."></script>
...
</html>-->
</head></html>
is inserted between the firsthook.min.js
script and the second no-hook script, which looks strange but is required for correct execution of no-hook scripts.- If
</head></html>
is inserted at the end of mandatory no-hook scripts according to the normal HTML format, the page encounters the unexpected "hook is not defined" error, whose root cause is under investigation.
- If
- Functions
- Object Shorthand Methods (
{ m() {} }
) - ES6 Classes (
constructor
,super
,this
,new
) - ES6 Modules (
import
,export
); - Expressions in Template Literals(
`${(v => v * v)(x)}`
) - Generator Functions (
function *g() { yield X }
) - Arrow Functions (
a => a
,a => { return a; }
,a => ({ p: a })
) - Async Functions (
async function f() {}
,async method() {}
,async () => {}
) - Default Parameters for Functions/Methods/Arrow Functions
- Default Parameters with Destructuring (
function f([ a = 1 ], { b = 2, x: c = 3 }) {}
) - Property Accessors (
o.p
,o['p']
,o.p()
)
bower install --save thin-hook
npm install --save thin-hook
<!-- browserified along with espree and escodegen; minified -->
<script src="path/to/bower_components/thin-hook/hook.min.js"></script>
const hook = require('thin-hook/hook.js');
hook(code: string, hookName: string = '__hook__', initialContext: Array = [], contextGeneratorName: string = 'method', metaHooking: boolean = true, hookProperty: boolean = true, sourceMap: object = null, asynchronous: boolean = false, compact: boolean = false, hookGlobal: boolean = true, hookPrefix: string = '_p_', initialScope: object = null)
code
: input JavaScript as stringhookName
: name of hook callback functioninitialContext
: typically[ ['script.js', {}] ]
contextGeneratorName
: function property name inhook.contextGenerators
- argument
astPath = [ ['script.js', {}], ['root', rootAst], ['body', bodyAst], ..., [0, FunctionExpressionAst] ]
- argument
metaHooking
: Enable meta hooking (run-time hooking of metaprogramming) if truehookProperty
: Enable hooking of object property accessors and new operators if truesourceMap
: Source map parameter in an object.{ pathname: 'path/to/script_source.js'}
Default: nullasynchronous
: Return a Promise if true. Default: falsecompact
: Generate compact code if true. Default: false- Note:
sourceMap
is disabled whencompact
is true
- Note:
hookGlobal
: Hook global variable access. Must be enabled withhookProperty
. Default: truehookPrefix
: Prefix forhook.global()._p_GlobalVariable
proxy accessors. Default:_p_
- Note:
hook.global()
return the global object withget/set
accessors for the prefixed name
- Note:
initialScope
: Initial scope object ({ vname: true, ... }
) for hooked eval scripts. Default: null
$hook$
:$hook$ === hook
. Alias ofhook
in hooked scriptshook.hookHtml(html: string, hookName, url, cors, contextGenerator, contextGeneratorScripts, isDecoded, metaHooking = true, scriptOffset = 0, _hookProperty = true, asynchronous = false)
hook.__hook__(f: function or string, thisArg: object, args: Array, context: string, newTarget: new.target meta property)
- minimal hook callback function with property hooking
f
:function
: target function to hookstring
: property operation to hook.
: get property (o.prop
)*
: iterate over (for (p in o)
,for (p of o)
)in
: property existence ('p' in o
)()
: function call (o.func()
)=
,+=
, ...: assignment operation (o.prop = value
)p++
,++p
,p--
,--p
: postfixed/prefixed increment/decrement operation (o.prop++
)delete
: delete operation (delete o.prop
)s.
: get property of super (super.prop
)s()
: call super method (super.method()
)s=
,s+=
, ...: assignment operation for super (super.prop = value
)s++
,++s
,s--
,--s
: postfixed/prefixed increment/decrement operation for super (super.prop++
)w.
,w=
,w()
,w++
, ...: operations on variables in withinwith
statements
thisArg
:this
object for the function or the operationargs
:- arguments for the function
[ property ]
for property access operations[ property, value ]
for property assignment operations[ property, [...args] ]
for function call operations
context
: context in the scriptnewTarget
:new.target
meta property for constructor calls;true
for new calls- Falsy values for non-
new
operations for faster detection of the operationsfalse
forwith
statement calls0
for function callsundefined
for other calls
hook.__hook_except_properties__(f, thisArg, args, context, newTarget)
- minimal hook callback function without property hooking
hook.hookCallbackCompatibilityTest(__hook__ = window[hookName], throwError = true, checkTypeError = true)
- run-time test suite for hook callback function
- Usage:
window.__hook__ = function __hook__ (...) {}; hook.hookCallbackCompatibilityTest();
- An error is thrown on compatibility test failure.
false
is returned on a test failure ifthrowError = false
- tests on non-callable object's function call are skipped if
checkTypeError = false
hook.contextGenerators
: object. Context Generator Functionsnull()
: context as''
astPath(astPath: Array)
: context as'script.js,[root]Program,body,astType,...'
method(astPath: Array)
: context as'script.js,Class,Method'
with caching, including computed method variable namecachedMethod(astPath: Array)
: alias formethod
cachedMethodDebug(astPath: Array)
: context as'script.js,Class,Method'
, comparing contexts with those by "oldMethod" in console.warn() messagesoldMethod(astPath: Array)
: context as'script.js,Class,Method'
for compatibility- custom context generator function has to be added to this object with its unique contextGeneratorName
hook.$(symbolToContext = __hook__, contexts)
: context symbol generator function used in hooked scripts to generate symbols corresponding to given contexts- Example call inserted at the beginning of a hooked script:
const __context_mapper__ = $hook$.$(__hook__, [ 'examples/example2.js,C', ... ]);
__context_mapper__
:Array
of symbol contexts- In a hooked script,
__context_mapper__
is actually__ + hex(sha256(topContextOfScript + code)) + __
- Note: Due to this specification, the same script in the same URL cannot be loaded to a single document multiple times
__context_mapper__[N]
: the symbol context corresponding to the string contextcontexts[N]
__hook__[__context_mapper__[N]]
is set ascontexts[N]
so that__hook__
can convert symbol contexts to their corresponding string contexts
- In a hooked script,
- Example call inserted at the beginning of a hooked script:
- Hooked Native APIs: Automatically applied in
hook()
preprocessinghook.global(hookCallback: function = hookName, context: string, name: string, type: string)._p_name
: hooked global variable accessor whenhookGlobal
is truetype
: one of'var', 'function', 'let', 'const', 'class', 'get', 'set', 'delete', 'typeof'
hook.Function(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName)
: hooked Function constructor for use in hook callback function__hook__
- Usage:
(new (hook.Function('__hook__', [['window,Function', {}]], 'method'))('return function f() {}'))()
- Notes:
- Avoid replacing the native API
window.Function
for better transparency (now commented out in thedemo/hook-native-api.js
) - NOT automatically applied in the hooking
- Applied in the hook callback function (
__hook__
) instead
- Avoid replacing the native API
- Usage:
hook.FunctionArguments(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName = 'method', args, isGenerator = false)
: generate hooked Function arguments to hand to Function constructor for use in hook callback function__hook__
- Usage:
hook.FunctionArguments('__hook__', [['window,Function', {}]], 'method', ['return function f() {}'])
- Returns hooked
args
in a clonedArray
- Usage:
hook.eval(hookName, initialContext: Array = [['eval', {}]], contextGeneratorName)
: hooked eval function- Usage:
hook.eval('__hook__', [['eval', {}]], 'method'))('1 + 2', (script, eval) => eval(script))
- Note: In no-hook scripts with the hooked global
eval
function viahook.hook(hook.eval(...))
, the evaluation is bound to the global scope unless the wrapper arrow function(script, eval) => eval(script)
is defined in the local scope and specifed as the second argument of eacheval()
call
- Usage:
hook.setTimeout(hookName, initialContext: Array = [['setTimeout', {}]], contextGeneratorName)
: hooked setTimeout function- Note: Not automatically applied if the first argument is an (arrow) function expression
hook.setInterval(hookName, initialContext: Array = [['setInterval', {}]], contextGeneratorName)
: hooked setInterval function- Note: Not automatically applied if the first argument is an (arrow) function expression
hook.Node(hookName, initialContext: Array = [['Node', {}]], contextGeneratorName)
: hooktextContent
propertyset textContent
: hooked with context 'ClassName,set textContent'
hook.Element(hookName, initialContext: Array = [['Element', {}]], contextGeneratorName)
: hooksetAttribute
functionsetAttribute('onXX', '{script in attribute}')
: Script in onXX handler attribute is hookedsetAttribute('href', 'javascript:{script in URL}')
: Script in URL"javascript:{script in URL}"
is hooked
hook.HTMLScriptElement(hookName, initialContext: Array = [['HTMLScriptElement', {}]], contextGeneratorName)
: HTMLScriptElement with hooked properties- Note: Applied only at run time. Not applied in preprocessing.
HTMLScriptElement
class is the same object as the native one.hook.Node
andhook.Element
are called internally. set textContent
: Script intextContent
is hooked iftype
is a JavaScript MIME type.Node.textContent
is hooked as well.- Note: Scripts set by
innerHTML
/outerHTML
/text
properties are NOT executed, whiletext
should be executed according to the standards.
- Note: Scripts set by
set type
: Script inthis.textContent
is hooked iftype
is a JavaScript MIME type.setAttribute('type', mimeType)
: Script inthis.textContent
is hooked ifmimeType
is a JavaScript MIME type.Element.setAttribute
is hooked as well.
- Note: Applied only at run time. Not applied in preprocessing.
hook.HTMLAnchorElement(hookName, initialContext: Array = [['HTMLAnchorElement', {}]]), contextGeneratorName)
: HTMLAnchorElement with hooked href propertyset href
: Script in URL"javascript:{script in URL}"
is hooked
hook.HTMLAreaElement(hookName, initialContext: Array = [['HTMLAreaElement', {}]]), contextGeneratorName)
: HTMLAreaElement with hooked href propertyset href
: Script in URL"javascript:{script in URL}"
is hooked
hook.Document(hookName, initialContext: Array = [['Document', {}]], contextGeneratorName)
: hookwrite
functionwrite('<sc' + 'ript>{script in string}</sc' + 'ript>')
: Script in HTML fragment is hooked
hook.with(scope: Object, ...scopes: Array of Object)
: Hookwith
statement scope objectwith (hook.with(obj, { v1: true, v2: true, ...})) {}
hook.importScripts()
: return hookedimportScripts
function for Workers, invalidating extensions other than.js
and.mjs
- Note: No arguments to pass
hook.hook(target: Class, ...)
: hook platform global object withtarget
- Usage:
['Function','setTimeout','setInterval',...].forEach(name => hook.hook(hook.Function('__hook__', [[name, {}]], 'method'))
- Usage:
hook.serviceWorkerHandlers
: Service Worker event handlersinstall
: 'install' event handler. Set version from theversion
parameteractivate
: 'activate' event handler. Clear caches of old versions.message
: 'message' event handler.- INTERNAL
'channel'
message: Transfer MessageChannel port objects for hook workers from the main document to the Service Worker at initialization - INTERNAL
'unload'
message: Trigger unloading of hook workers - INTERNAL
'coverage'
message: Transfer__coverage__
instanbul coverage object for the Service Worker to the main document to collect code coverage intest/hook.min.js
['plugin', 'pluginId', ...params ]
message: Transfer a message to the target plugin identified by'pluginId'
. The target plugin must add its own event listener to handle the message.['plugin', 'pluginId:enqueue', ...params ]
: When thepluginId
ends with:enqueue
, events with posted messages are enqueued tohook.parameters.messageQueues['pluginId:enqueue'] = []
even before plugins are loaded into the Service Worker- Each enqueued message is immediately responded via
event.ports[0].postMessage()
with a dummy response message generated by cloning the posted message and appending':enqueued'
such as['plugin', 'pluginId:enqueue', ...params, ':enqueued' ]
- The target plugin must dequeue the enqueued events and append
':dequeued'
to the queue to stop further enqueueing. For example, the queue[]
changes as follows:- An event is enqueued:
[ event1 ]
- The plugin append
':dequeued'
:[ event1, ':dequeued' ]
- The plugin dequeues and processes the event(s):
[ ':dequeued' ]
- An event is enqueued:
- Enqueued messages are likely to be one-way messages as the main document is about to reload itself
hook.parameters.messageQueues['pluginId:enqueue']
may NOT exist when the plugin is loaded. So the plugin must create its own queue if it has not been created.
- Each enqueued message is immediately responded via
- INTERNAL
fetch
: 'fetch' event handler. Cache hooked JavaScripts and HTMLs except for the main page loadinghook.min.js
<script src="thin-hook/hook.min.js?version=1&sw-root=/&no-hook=true&hook-name=__hook__&discard-hook-errors=true&fallback-page=index-no-sw.html&hook-property=true&service-worker-ready=true"></script>
: arguments from the pageversion
: default1
. Service Worker cache version. Old caches are flushed when the version is changed in the main page and reloaded. Service Worker is updated when the controlled page is detached after the reloading.sw-root
: optional. Set Service Worker scopehook-name
: default__hook__
. hook callback function namecontext-generator-name
: defaultmethod
. context generator callback function namediscard-hook-errors
:true
if errors in hooking are ignored and the original contents are provided. Default:true
fallback-page
: fallback page to land if Service Worker is not available in the browserno-hook-authorization
: Optional. CSV of no-hook authorization tickets for no-hook scripts. Typically for ticket of no-hook authorization script itself.- The values are stored in
hook.parameters.noHookAuthorizationPreValidated
object in Service Worker - Add the value
log-no-hook-authorization
to log authorization in console - Note:
no-hook-authorization
must not exist in learning mode withhook.parameters.noHookAuthorization['*'] === true
- Steps to update authorized no-hook scripts:
-
- Let no-hook be "learning mode" by truthy
hook.parameters.noHookAuthorization['*']
- Let no-hook be "learning mode" by truthy
-
- Remove (or temporarily rename)
no-hook-authorization
parameter from hook.min.js
- Remove (or temporarily rename)
-
- Update no-hook script(s)
-
- Clear Service Worker(s)
-
- Update
version
parameter for hook.min.js
- Update
-
- Check "Preserve Logs" option in debugger console
-
- Reload the page(s) with no-hook script(s)
-
- Copy and Paste values of hook.parameters.noHookAuthorizationPassed from both browser document and Service Worker to no-hook authorization script
-
- Disable "learning mode"
-
- Enable (or revive)
no-hook-authorization
parameter for hook.min.js with a dummy value
- Enable (or revive)
-
- Clear Service Worker(s)
-
- Update
version
parameter for hook.min.js
- Update
-
- Reload the page(s) with no-hook scripts(s)
-
- Copy and Paste the ticket for the no-hook authorization script into the
no-hook-authorization
parameter
- Copy and Paste the ticket for the no-hook authorization script into the
-
- Update
version
parameter for hook.min.js
- Update
-
- Clear Service Worker(s)
-
- Reload the page(s) with no-hook script(s)
-
- Check if there are no unauthorized no-hook scripts
-
- Steps to update authorized no-hook scripts:
- The values are stored in
hook-property
:hookProperty
parameter.true
if property accessors are hooked. The value affects the default value of thehookProperty
parameter forhook()
hook-global
:hookGlobal
parameter.true
if global variables are hooked. The value affects the default value of thehookGlobal
parameter forhook()
hook-prefix
:hookPrefix
parameter. Prefix accessor names ofhook.global()._p_GlobalVariableName
with the value. Default:_p_
compact
:compact
parameter. Generate compact code iftrue
. The value affects the default value of thecompact
parameter forhook()
service-worker-ready
:true
if the entry HTML page is decoded;false
if encoded. This parameter must be at the end of the URL
<script src="script.js?no-hook=true"></script>
: skip hooking for the source script<script no-hook>...</script>
: skip hooking for the embedded script<script context-generator>
: register a custom context generator for both Service Worker and browser document<script context-generator no-hook>hook.contextGenerators.custom = function (astPath) {...}</script>
: embedded script<script context-generator src="custom-context-generator.js?no-hook=true"></script>
: with src URL- Valid only in the main entry document with
hook.min.js
for Service Worker - Must be runnable in both Service Worker and browser document
- Defined variables for context generator scripts in Service Worker
version
variable: cache name as a stringversion_{version number}
- Note: In Service Worker,
'version_' + new URL(location.href).searchParams.get('version')
might be incorrect since Service Worker for the old version before version upgrading might still be running for the new version. In contrast,'version_' + new URL(document.querySelector('script').src).searchParams.get('version')
in the main document is always up-to-date.
- Note: In Service Worker,
- Extensions other than context generators:
- Set Service Worker parameters:
hook.parameters.cors = [ 'cors_url_1', 'cors_url_2', ... ]
: specify CORS script URLshook.parameters.cors = [ (url) => url.match(/cors_url_pattern/), ... ]
: specify CORS script URL detector function(s)hook.parameters.opaque = [ 'opaque_url_1', 'opaque_url_2', ... ]
: specify authorized opaque URLshook.parameters.opaque = [ (url) => url.match(/opaque_url_pattern/), ... ]
: specify authorized opaque URL detector function(s)
- Set
no-hook
Authorization Tickets:hook.parameters.noHookAuthorization = { '{sha-256 hex hash for authorized no-hook script}': true, ... }
: Set keys fromhook.parameters.noHookAuthorizationPassed
in both Document and Service Worker threadshook.parameters.noHookAuthorization = { '*': true }
: learning mode to detect authorization tickets
- Specify URL patterns for
no-hook
scripts:hook.parameters.noHook = [ 'no_hook_url_1', 'no_hook_url_2', ... ]
: specifyno-hook
script URLshook.parameters.noHook = [ (url: URL) => !!url.href.match(/{no-hook URL pattern}/), ... ]
: specifyno-hook
script URL detector function(s)
- Specify URL patterns for source map target scripts:
hook.parameters.sourceMap = [ 'source_map_target_url_1', 'source_map_target_url_2', ... ]
: specify source map target script URLshook.parameters.sourceMap = [ (url: URL) => !!url.href.match(/{source map target URL pattern}/), ... ]
: specify source map target script URL detector function(s)
- Specify URL for hook worker script:
hook.parameters.hookWorker = 'hook-worker.js?no-hook=true'
: specify hook worker script URL
- Register Custom Event Handler:
if (typeof self === 'object' && self instanceof 'ServiceWorkerGlobalScope') { self.addEventListener('{event_type}', function handler(event) {...})}
- URL for the entry page
hook.parameters.baseURI
: Set indemo/bootstrap.js
- Empty Document URL
hook.parameters.emptyDocumentUrl = new URL('./empty-document.html', baseURI);
: Set indemo/bootstrap.js
.<iframe src="empty-document.html?url=https://host/path.html,iframe">
to specify context in iframe document
- Bootstrap Script Tag
hook.parameters.bootstrap = "<script>frameElement.dispatchEvent(new Event('srcdoc-load'))</script>";
: Set indemo/bootstrap.js
- Append to the hooked
srcdoc
to dispatchsrcdoc-load
event toonload
handler
- Onload Wrapper Script
hook.parameters.onloadWrapper = "event.target.addEventListener('srcdoc-load', () => { $onload$ })";
: Set indemo/bootstrap.js
- Receive
srcdoc-load
event and trigger the originalonload
script- Note:
addEventListener('load', handler)
is currently called BEFORE the document fromsrcdoc
is loaded andsrcdoc-load
event is fired.
- Note:
- Virtual Blob URL (disabled by default)
hook.parameters.virtualBlobUrlTargetType = new Map([['text/html', 'file.html'],['text/javascript', 'file.js'],['image/svg+xml', 'file.svg']]);
: Set indemo/bootstrap.js
to specify target MIME types and their corresponding virtual Blob URL file nameshook.parameters.virtualBlobBaseUrl = null;//new URL('blob/', hook.parameters.baseURI).href;
: Set indemo/bootstrap.js
to specify the base URL for virtual Blob URLs- Convert Blob URLs to Virtual Blob URLs in https so that they can be preprocessed in Service Worker and set in attributes
- Original Blob URL:
blob:https://origin.site/abcd...1234
fortext/html
Blob object - Virtual Blob URL:
https://origin.site/entry/blob/file.html?bloburl=blob:https://origin.site/abcd...1234
- Original Blob URL:
- If these parameters are not configured (which is default), no conversion will be performed on Blob URLs
- Flag to block
<embed>
and<object>
elementshook.parameters.hangUpOnEmbedAndObjectElement = false;
: Set indemo/bootstrap.js
- If the flag is set as
true
, the application hangs up on encountering activities by<object>
and<embed>
elements- To use this flag,
hook.parameters.mutationObserver
andhook.parameters.mutationObserverConfig
must be set indemo/hook-callback.js
- To use this flag,
- Empty SVG to load the target URL
hook.parameters.emptySvg = '<?xml version="1.0"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1px" height="1px"><script>location = "$location$";</script></svg>';
- If
hook.parameters.hangUpOnEmbedAndObjectElement = true
, the SVG loadsabout:blank
- If
- Bootstrap Scripts for SVG
hook.parameters.bootstrapSvgScripts = '<script xlink:href="URL?params"></script>...'
- Flag to omit superfluous closing tags
hook.parameters.omitSuperfluousClosingHtmlTags = true
:true
to omit superfluous closing tags; Set indemo/bootstrap.js
false
to be compatible with old versions- In SVG, tag names and attribute names are case-sensitive. If the flag is
false
, SVG images with case-sensitive tags and attributes can be broken. - If the flag is set as
true
, the output should have minimal required changes from the original HTML or SVG - Note on Side-Effects: Policy contexts for inline scripts can contain
/path/script.js,script@{pos}
, which can vary with this flag set astrue
- Check Request callback on Fetch at Service Worker
hook.parameters.checkRequest = async function (event, response, cache) { /* check request */ return response ; }
:response
- cached response if exists; Seedemo/disable-devtools.js
- Check Response callback on Fetch at Service Worker
hook.parameters.checkResponse = async function (event, request, response, cache) { /* check response */ return response ; }
:response
- just fetched response; Not called if a cache response exists
- List of Asynchronous Tasks before Service Worker registration
hook.parameters.preServiceWorkerTasks
: The first task is checked afterDOMContentLoaded
event; Therefore, the first taskPromise
must be pushed beforeDOMContentLoaded
event- If the last task resolves to a constant string
"skipServiceWorkerRegistration"
, the default Service Worker registration processes are skipped and the last task takes the responsiblity of the Service Worker registration and reloading the entry page. Even after the Service Worker registration completed and the page is reloaded, this"skipServiceWorkerRegistration"
value is effective for the task so thathook.min.js
can complete remaining tasks such as starting the ping service and the hook workers. - 3 Acceptable Data Types
Promise
: A singlePromise
object; Equivalent to[ Promise ]
;[ Promise, Promise, ...]
:Array
ofPromise
objectsAsync Iterable
:Async Iterator
implementingtasks[Symbol.asyncIterator]
protocol
- If the last task resolves to a constant string
- Callback on Errors from
hook.parameters.preServiceWorkerTasks
hook.parameters.onPreServiceWorkerTasksError = async function onError(exception) {}
: Asynchronous function to handle the exception- Default:
window.location = 'about:blank';
- Default:
- Callback to Decode Entry HTML in a plugin
hook.parameters.decodeEntryHtml = async function decodeEntryHtml(event, request, response, cache, original, decoded)
event: FetchEvent
: Event for the fetch requestrequest: Request
: Request object that fetched the contentresponse: Response
: Response object of the fetchcache: Cache
: Cache object for the current versionoriginal: String
: original entry page HTMLdecoded: String
:= hook.serviceWorkerTransformers.decodeHtml(original)
- return value:
String
decoded entry page HTML to respond to the document- The function can just return
original
ordecoded
while it can also modify the content depending on the situation.
- The function can just return
- Optional headers to include in cache response headers
hook.parameters.significantHeaders = { "Header-Name": true }
- Additional Cacheable Content-Types
hook.parameters.cacheableContentTypes = { "text/css": true, "image/png": true, ... }
- Note:
text/html
,text/javascript
,image/svg+xml
must not be included here
- Note:
- Callback to Validate Cacheable URL
hook.parameters.validateCacheableUrl = function (url, contentType)
url: String
: target URL to validatecontentType: String
: normalized Content-Type without charset- return a truthy value if
url
withcontentType
is cacheable - If the callback function is undefined, any
contentType
values withinhook.parameters.cacheableContentTypes
are cacheable
- Root of Application Path
hook.parameters.appPathRoot = '/';
- The app assets are underlocation.origin + hook.parameters.appPathRoot
- Script Hashes
hook.parameters.scriptHashes = { "SHA256(authorized inline script)": "context", ... }
- List of hashes for authorized inline scripts
- Integrity
hook.parameters.integrity = { "URL path": "base64(SHA256(response data))", ... }
- List of integrity for static contents
- MutationObserver
hook.parameters.mutationObserver = new MutationObserver(observerCallback);
-MutationObserver
object set indemo/hook-callback.js
hook.parameters.mutationObserverConfig = { childList: true, subtree: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, };
- Configuration options forhook.parameters.mutationObserver.observe(options)
set indemo/hook-callback.js
- Note: They are used in the wrapped
Node.attachShadow()
to track mutations in every shadow DOM as well as for all document objects of windows and frames
- Note: They are used in the wrapped
- Tracker Callback
hook.parameters.innerHTMLTracker = function (node, value, processed) {}
: Set indemo/hook-callback.js
for mutation detection- Track each
Element.innerHTML
operation before performing it
- Import Maps
hook.parameters.importMapsJson = "{ JSON string for Import Maps }"
: Optional import maps object in JSON string.hook.parameters.importMapper(specifier, scriptURL)
: Wrapper function for import maps. Resolve modulespecifier
fromscriptURL
- Resolution of bare specifiers for ES modules can be disabled by setting this function as
hook.parameters.importMapper = null
- Resolution of bare specifiers for ES modules can be disabled by setting this function as
hook.parameters.moduleDependencies = {}
: Optional object to dump module dependencies for hooked modules
- Set Service Worker parameters:
- register as Service Worker
Service-Worker-Allowed
HTTP response header must have an appropriate scope for the target application
cors=true
parameter: CORS script, e.g.,<script src="https://cross.origin.host/path/script.js?cors=true"></script>
hook.serviceWorkerTransformers
:encodeHtml(html: string)
: encode HTML for Service Worker<!-- end of mandatory no-hook scripts -->
: insert this exact marker as a comment so that all mandatory no-hook scripts before the marker in the HTML of the entry document can be executed even at the first load without Service Worker- Note:
no-hook-authorization
hashes are NOT effective at the first load
- Note:
decodeHtml(html: string)
: decode encoded HTML for Service Worker
hook.hookWorkerHandler(event)
: onmessage handler for Hook Workers- Usage:
onmessage = hook.hookWorkerHandler
in Hook Worker script
- Usage:
hook.registerServiceWorker(fallbackUrl: string = './index-no-service-worker.html', reloadTimeout: number = 500, inactiveReloadTimeout: number = 1000)
:- Automatically called on loading
hook.min.js
on browsers fallbackUrl
: fallback URL for browsers without Service WorkerreloadTimeout
: default: 500 (ms). Timeout to reload the page when no Service Worker is detectedinactiveReloadTimeout
: default: 1000 (ms). Timeout to reload the page when inactive (waiting, installing) Service Worker is detected. When a state change of the Service Worker instance is detected, the page is reloaded immediately even before the timeout.
- Automatically called on loading
utils
: UtilitiescreateHash
: Synchronous SHA hash generator collections from sha.jsHTMLParser
: HTML parser from htmlparser2importMaps
: Forked reference implementation of Import mapsparseFromString(importMapsJsonString, baseURL)
: Parser of import maps JSON string atbaseURL
. ReturnparsedImportMap
object forresolve()
resolve(specifier, parsedImportMap, scriptURL)
: Resolver ofspecifier
forscriptURL
based onparsedImportMap
- Plugins are no-hook scripts for enhancements
- Currently, they are configured for the demo application under
demo/
, but fully customizable for any target applications
- Currently, they are configured for the demo application under
- Configurations
hook.parameters.noHookAuthorization = { "hex sha256 digest for no-hook script": true, ... }
- Hex sha256 digests have to be updated in the build process
- See
update-no-hook-authorization
gulp task
- See
- Hex sha256 digest of the
no-hook-authorization.js
script itself has to be set as a parameter forhook.min.js
<script src="../../thin-hook/hook.min.js?version=496&no-hook-authorization=6a83335a7630118516213f52715a24520efc7030b3562291e92a06482894b95e&service-worker-ready=false"></script>
- See
update-no-hook-authorization-in-html
gulp task
- Hex sha256 digests have to be updated in the build process
hook.parameters.sourceMap = [...]
hook.parameters.hookWorker = 'hook-worker.js?no-hook=true';
- Features
- Check integrity of the browser agent
- Check integrity of the loaded scripts
- Establish and update secure connection to
integrityService.js
- Check integrity of requests and responses
- Encrypt request body data
- Decrypt response body data
- Check integrity of Service Worker cache contents by appending and verifying
x-cache-*
headers - TBD
- Configurations
- TBD
- Features
- Force redirection to
about:blank
when the user tries to open Developer Tools - Force redirection to
about:blank
when the user tries to inspect a source code of the pages
- Force redirection to
- Configurations
const devtoolsDisabled = true
: Usefalse
and rebuild withgulp demo
to enable Dev Tools- Configurable at
targetConfig.mode.devtoolsDisabled
indemo-config/config.js
- Configurable at
- Configurations
hook.contextGenerators.hash
: an example custom context generator (not used for demo)hook.contextGenerators.method2
: an example custom context generator (not used for demo)Object.freeze(hook.contextGenerators)
- Configurations
hook.parameters.emptyDocumentUrl
hook.parameters.bootstrap
hook.parameters.onloadWrapper
hook.parameters.virtualBlobUrlTargetType
hook.parameters.virtualBlobBaseUrl
hook.parameters.hangUpOnEmbedAndObjectElement
hook.parameters.emptySvg
hook.parameters.bootstrapSvgScripts
hook.parameters.noHookAuthorizationParameter
: Value ofhook.min.js?no-hook-authorization
parameter used inhook-callback.js
hook.parameters.noHookAuthorizationFailed = {}
hook.parameters.noHookAuthorizationPassed = {}
- Configurations
hook.parameters.cors
hook.parameters.opaque
hook.parameters.worker
(Ineffective and unused for now)
-
Features
- Fetch
cache-bundle.json
and store the contents intocaches
- Format:
{ "version": "version_XXX", "same origin URL path (absolute)": "text data", ..., "absolute URL": "text data", ... }
- Basic MIME types:
.js
:application/json
.html
:text/html
.json
:application/json
.svg
:image/svg+xml
- Extended Metadata Format: See
demo/cache-bundle.json
- key:
"url?param=2": Object
- property:
"Location": "url?param=1"
- link to the other content to eliminate redundant identical body data for multiple URLs- Note: If Non-dataURI
"Location"
exists, other metadata entries are ignored
- Note: If Non-dataURI
- property:
"Location": "data:image/jpeg;base64,..."
- encoded body data for non-textual contents- Note:
"Location"
appears only once in a metadata object, of course
- Note:
- property:
"Content-Type": "text/xml"
- MIME type - property:
"body": "body in string"
- content body - property:
"Other-Headers": "header value"
- additional significant HTTP headers specified inhook.parameters.significantHeaders
- property:
- key:
- Format:
- Generate
cache-bundle.json
fromcaches
and upload the data to saveURL (errorReport.json
) if the entry page is invoked with?cache-bundle=save
parameter- The server must be
npm run upload
withcacheBundleUploadService.js
to receive and savecache-bundle.json
- Parameters:
{ "type": "cache-bundle.json", "data": "stringified cache-bundle.json" }
- The server must be
- Automate generation of
cache-bundle.json
- Trigger automation by
cacheBundleGeneration.js
viapuppeteer
- Invoked via
cache-bundle
gulp task
- Invoked via
- Fetch a special
cache-bundle.json
at build time- Generated by
cache-bundle-automation-json
gulp task - Format:
"version": "version_123"
: version obtained viaget-version
gulp task"https://thin-hook.localhost.localdomain/automation.json":
:JSON.stringify()
with the object with the following properties"state": "init"
: update state in the script to perform operations including reloading"serverSecret": serverSecret
: one-time build-time-only secret for validatingcache-automation.js
script"script": cacheAutomationScript
: contents ofcache-automation.js
script
- Generated by
cache-automation.js
: script for collecting caches by automatically navigating the target applicationcache-automation.js
script is hooked with the contexthttps://thin-hook.localhost.localdomain/automation.json,*
- ACL has to be defined for
cache-automation.js
- ACL has to be defined for
- Cache cleanup and page reload are done before
cache-automation.js
execution - Cache bundle generation is performed after
cache-automation.js
execution- Metadata are processed and redundant body data are converted to links to other contents with the same body data within
cache-bundle.json
- Metadata are processed and redundant body data are converted to links to other contents with the same body data within
- Trigger automation by
- Fetch
-
Configurations
const enableCacheBundle = true
: Usefalse
and rebuild withgulp demo
to disablecache-bundle
- For extended metadata for
cache-bundle.json
hook.parameters.significantHeaders = { "Header-Name": true }
: optionalhook.parameters.cacheableContentTypes = { "text/css": true, "image/png": true, ... }
: optionalhook.parameters.validateCacheableUrl = function (url, contentType)
: optional
- For Service Worker
const cacheBundleURL = new URL('cache-bundle.json', hook.parameters.baseURI);
const saveURL = new URL('errorReport.json', hook.parameters.baseURI);
?authorization=
:hex(sha256(serverSecret + cache-automation.js script))
- Set via
encode-demo-html
gulp task
- Set via
- For automated generation of
cache-bundle.json
cache-automation.js
must be fully customized for the target application- ACL for
cache-automation.js
with the contexthttps://thin-hook.localhost.localdomain/automation.json,*
- Features
- ACL for objects in HTML documents, SVG, Worker, SharedWorker
- Maintain
contextStack
withStack
class objectStack
class object is a brancheable linked list withpush/pop
operations- The branching feature of
Stack
is not utilized for now
- The branching feature of
- Call
hook.hookCallbackCompatibilityTest()
- Attach MutationObserver to audit URLs and elements in DOM mutations
- Block
blob:
URLs except for downloading to local files - Block unauthorized DOM mutations suspectedly from browser extensions
- On detection, an alert message Blocked on Browser Extensions is shown and the application hangs up
- Block
- Hook global objects
- Via
hooked = hook[name](Symbol.for('__hook__'), [[name, { random: name === 'Node' }]], 'method')
Object.defineProperty(_global, name, { value: hooked, configurable: true, enumerable: false, writable: false });
- Target global object names
eval
setTimeout
setInterval
Node
Element
HTMLScriptElement
HTMLIFrameElement
HTMLObjectElement
HTMLEmbedElement
HTMLAnchorElement
HTMLAreaElement
Document
importScripts
- Via
- Prohibit global object access via automation like puppeteer
- Return
undefined
on prohibited global object access - Forced redirection to
about:blank
on prohibited global object access
- Return
- Configurations
- For ACL
__hook__
: hook callback functionObject.defineProperty(_global, '__hook__', { configurable: false, enumerable: false, writable: false, value: hookCallbacks.__hook__ });
hookCallbacks.__hook__
: full features (acl + contextStack + object access graph)hookCallbacks.__hook__acl
: acl only (acl + contextStack) - defaulthookCallbacks.__hook__min
: minimal (no acl)
contextNormalizer
andacl
- Configurable at
demo-config/policy/policy.js
and included policy modules
- Configurable at
- For MutationObserver
hook.parameters.mutationObserver = new MutationObserver(observerCallback);
-MutationObserver
object set indemo/hook-callback.js
hook.parameters.mutationObserverConfig = { childList: true, subtree: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, };
- Configuration options forhook.parameters.mutationObserver.observe(options)
set indemo/hook-callback.js
hook.parameters.innerHTMLTracker = function (node, value, processed) {}
- Tracker callback to detect coming DOM mutations from settingElement.innerHTML
const detectDOMIntrusion = true;
- Usetrue
to detect DOM intrusionconst messagesOnUnauthorizedMutation = { en: 'Blocked on Browser Extensions' };
- Alert messages on DOM intrusion detection, indexed fornavigator.language
- For global object access
const enableDebugging = false
: Usetrue
to enable debugging by disabling forced redirection toabout:blank
on prohibited global object access- Configurable at
targetConfig.mode.enableDebugging
indemo-config/config.js
- Configurable at
const wildcardWhitelist
:Array
ofRegExp
for Chrome browser'snew Error().stack
format- Configurable at
demo-config/policy/wildcardWhitelist.js
- Example configurations for demo
new RegExp('^at (.* [(])?' + origin + '/components/'), // trust the site contents including other components
new RegExp('^at ([^(]* [(])?' + 'https://cdnjs.cloudflare.com/ajax/libs/vis/4[.]18[.]1/vis[.]min[.]js'),
new RegExp('^at ([^(]* [(])?' + 'https://www.gstatic.com/charts/loader[.]js'),
- Configurable at
const excludes = new Set() : { 'window.Math' }
: excludeMath
object- Note:
Math
object properties must be wrapped withwrapGlobalProperty
function
- Note:
- For ACL
- Features
- Provide list of authorized hashes for inline scripts in HTML to accelerate preprocessing in HTML Imports polyfill
- Required in the entry page and HTML subdocuments
- The list of authorized hashes is generated in
gulp script-hashes
task and inserted intocache-bundle.json
with the key SCRIPT_HASHES_PSEUDO_URLhttps://thin-hook.localhost.localdomain/script-hashes.json
- The list is empty if
service-worker-ready=false
while it is copied fromcache-bundle.json
ifservice-worker-ready=true
- The list is stored at
hook.parameters.scriptHashes
- The list is stored at
- SRI
integrity
attribute requires 2 integrity values for bothservice-worker-ready=false
andservice-worker-ready=true
- They are generated in
gulp script-hashes-integrity
task and inserted into the entry page, i.e.,original-index.html
andindex.html
- They are generated in
- Unnecessary if HTML Imports feature is natively implemented in the browser or unused in the app
- It is recommended to append this plugin as the behaviors without the plugin are not well verified
- Configurations
- Mandatory parameter
service-worker-ready=false
for the entry page, which is automatically converted toservice-worker-ready=true
after preprocessed - Mandatory parameter
service-worker-ready=true
for other HTML pages includingempty-document.html
- Required gulp tasks:
script-hashes
,script-hashes-integrity
- Mandatory parameter
- Features
- Load the target HTML without the hook infrastructure scripts after the hook infrastructure scripts are loaded in
empty-document.html
foriframe
documents
- Load the target HTML without the hook infrastructure scripts after the hook infrastructure scripts are loaded in
- Configurations
- The container
iframe
element is automatically configured in preprocessing HTML contents- Parameter
content=base64URL(encodeURIComponent(HTML))
- HTML is written into the
iframe
document viadocument.write(HTML)
after preprocessing
- HTML is written into the
- Parameter
blob=encodeURIComponent(BlobURL)
- Blob is written into the
iframe
document as HTML viadocument.write(fetch(Blob))
after preprocessing if the blob type istext/html
- Blob is written into the
iframe
document as a plain text via data URL if the blob type istext/plain
- Blob is blocked if the blob type is
image/svg+xml
- Blob with other blob types is written into the
iframe
document as data URL
- Blob is written into the
- Parameter
- The container
- Features
- Wrap the remaining global objects which have not been wrapped in
hook-callback.js
- Put this script at the end of global API definitions after
hook-callback.js
- Use
window[Symbol.for('wrapGlobalProperty')]()
to wrap global objects- Defined in
hook-callback.js
- Defined in
- Wrap the remaining global objects which have not been wrapped in
- Configurations
- For global object access
const excludes = new Set(); [ 'Math' ].forEach(name => excludes.add(name));
- Set of excluded objects in wrapping
window.*
- Set of excluded objects in wrapping
- For global object access
- There are significant access performance overheads on global objects due to wrapped property getter/setter functions
- To mitigate the overheads, define local alias objects for frequently used global objects
- For example,
const URL = window.URL, RegExp = window.RegExp, ...
- For example,
- Internal Details on the overheads:
- If
contextStack
is empty, the global object is accessed outside of hooked scripts and thusnew Error().stack
has to be analyzed, which is an extremely heavy operation - If
contextStack
is not empty, the global object is accessed within a hooked script, whose access can be controlled via ACLcontextStack
operations are relatively lightweight without performance degradation on deep call stack
- If local alias objects are defined, the corresponding global object access is performed only once per object, whose overheads are insignificant
- If
- Features
- Mark the parsed elements in DOM with
node[Symbol.for('parsed')] = true
at the end of HTML body to filter out valid DOM mutations from invalid ones - In
iframe
documents, dispatchsrcdoc-load
event for the containingframeElement
- Mark the parsed elements in DOM with
- Configurations
- Insert the script at the end of the entry page HTML body
- Use the script in
hook.parameters.bootstrap
for theiframe
document wrapped viahook.parameters.emptyDocumentUrl
Server-side scripts and components configured for the demo but fully customizable for the target application
Back-end server for the demo. TBD
Handler for demo/errorReport.json
POST requests
Used at build time to automate generation of cache-bundle.json
via puppeteer
Formerly used at build time to automate uploading of cache-bundle.json
via a POST request
Express middleware for demoServer.js
to handle demo/postHtml
. This should be unnecessary and should not be used except for verification of HTML via a POST request.
Express middleware for demoServer.js
to provide integrity and double encryption of body data
demo-backend/whitelist.json
- list of URL paths which are allowed to access without encryptiondemo-backend/blacklist.json
- list of URL paths which are not allowed to access; namelydemo/index.html
- Generated in
gulp encode-demo-html
task by parsing the entry page HTML
- Generated in
Node addon package compiled from the C++ source binding.cpp
to provide the following functions
rsa_oaep_decrypt(ArrayBuffer encrypted, String private_key_pem)
- Decrypt ArrayBuffer data by RSA-OAEP-SHA256 with a String private key in PEM format viaopenssl
When invoked as a CLI script, it provides the validation server for ClientIntegrity.browserHash
. TBD
- API: TBD
demo-backend/validation-console/dist/
is served at its HTTPS rootdemo-keys/demoCA/${process.env["VALIDATION_HOST"]}.{key|crt}
is used for HTTPS server. Defaults tolocalhost:8082
demo-keys/demoCA/client.{key|crt}
are used for client certificate authentication
When imported as a package, it provides the client API for the validation server. TBD
demo-keys/demoCA/client.{key|crt}
are used for client certificate authentication
Validation Console GUI served by demo-backend/validationService.js
. TBD
Script to generate certificates in demo-keys/demoCA/
Key pairs and secret keys are stored for the application version.
{
"version": "version_668", // application version
"rsa-private-key.pem": "RSA PRIVATE KEY in PEM",
"rsa-public-key.pem": "RSA PUBLIC KEY in PEM",
"ecdsa-private-key.pem": "ECDSA PRIVATE KEY in PEM",
"ecdsa-public-key.pem": "ECDSA PUBLIC KEY in PEM",
"session-id-aes-key": "base64(random(32 bytes))",
"session-id-aes-iv": "base64(random(12 bytes))",
"scriptsHashHex": "hex(ClientIntegrity.scriptsHash)",
"htmlHashHex": "hex(ClientIntegrity.htmlHash)"
}
TBD
{
"scripts": {
"test": "wct",
"build": "gulp",
"demo": "run-p -l demoServer errorReportService validationService",
"debug": "run-p -l debugServer errorReportService validationService",
"https": "run-p -l httpsServer errorReportService validationService",
"upload": "run-p -l buildServer cacheBundleUploadService",
"cache-bundle": "run-p -r -l buildServer cacheBundleUploadService cacheBundleGeneration",
"updateHtmlHash": "run-p -r -l buildServer cacheBundleUploadService loadOnly",
"buildServer": "node demo-backend/demoServer.js -p 8080 -m build -P https -H \"localhost:8080\"",
"demoServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\"",
"httpsServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -P https -H \"${SERVER_HOST}:8080\"",
"debugServer": "node --inspect-brk=0.0.0.0:9229 demo-backend/demoServer.js -p 8080 -m debug -c 1 -H \"${SERVER_HOST}\"",
"postHtml": "run-p -l postHtmlServer errorReportService",
"postHtmlServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\" --middleware ./postHtml.js",
"errorReportService": "node demo-backend/errorReportService.js -p 8081",
"validationService": "node demo-backend/validationService.js -p 8082 -m server -H \"${VALIDATION_HOST}\"",
"integrity-service-helpers": "cd demo-backend/integrity-service-helpers && npm install",
"validation-console": "cd demo-backend/validation-console && npm ci && npm run build",
"demo-certificates": "cd demo-keys && ./generate_cert.sh ",
"clean-demo-certificates": "cd demo-keys && rm -riv demoCA",
"cacheBundleUploadService": "node demo-backend/cacheBundleUploadService.js",
"cacheBundleGeneration": "node demo-backend/cacheBundleGeneration.js",
"loadOnly": "node demo-backend/cacheBundleGeneration.js loadOnly",
"test:attack": "run-p -r -l buildServer cacheBundleUploadService puppeteerAttackTest",
"puppeteerAttackTest": "node test/puppeteerAttackTest.js",
"demo-frontend-modules": "cd demo/ && npm install",
"demo-frontend-modules-locked": "cd demo/ && npm ci"
}
}
HTTPS server host name for the application. Defaults to localhost
HTTPS server host name at port 8082 for Validation Console and Validation Service API. Defaults to localhost
Run hook tests
Build hook.min.js
and the demo via gulp
Serve the demo from demo-frontend/
at https://${SERVER_HOST}/components/thin-hook/demo/
via nginx proxing to http://localhost:8080
Serve the demo from demo/
at https://${SERVER_HOST}/components/thin-hook/demo/
via nginx proxying to http://localhost:8080
Serve the demo from demo-frontend/
at https://${SERVER_HOST}:8080/components/thin-hook/demo/
with the key pair demo-keys/demoCA/${SERVER_HOST}.{key|crt}
Formerly used to upload cache-bundle.json
via demo-backend/cacheBundleUploadService.js
at build time
Called from gulp cache-bundle-automation
task to automate building of cache-bundle.json
at build time
Called from gulp update-html-hash
task to update demo-keys/keys.json
for "htmlHashHex"
of the entry page HTML after the integrity
attribute of <script src="script-hashes.js">
is updated in gulp script-hashes-integrity
task
Called from npm run cache-bundle
to tonvoke demoServer.js
in build
mode at build time
Called from npm run demo
to invoke demoServer.js
in server
mode without TLS
Called from npm run https
to invoke demoServer.js
in server
mode with TLS
Called from npm run debug
to invoke demoServer.js
in debug
mode attached by Node.js debugger
Called from npm run {demo|https|debug}
to invoke errorReportService.js
at port 8081
Called from npm run {demo|https|debug}
to invoke validationService.js
at port 8082
Build demo-backend/integrity-service-helpers/
as Node addon API
Build demo-backend/validation-console/
Validation Console GUI, which is served via validationService.js
Generate certificates for the demo
npm run demo-certificates -- ${hostname}
- Server certificate atdemo-keys/demoCA/localhost.{crt|key}
;demo-keys/demoCA/demoCA.{crt|key}
if missingnpm run demo-certificates -- client client
- Client certificate atdemo-keys/demoCA/client.{crt|key|pfx}
;demo-keys/demoCA/demoCA.{crt|key}
if missing- A password has to be specified for the client certificate. The password must not be empty on some platforms.
- Automatically called from
gulp demo-certificates
task- Notes:
demo-keys/demoCA/demoCA.crt
must be trusted as a root CA by the local Chrome browser at build time- Installation on Linux:
cd demo-keys; certutil -d sql:$HOME/.pki/nssdb -A -n 'thin-hook demo CA' -i ./demoCA/demoCA.crt -t TCP,TCP,TCP
- Installation on Linux:
demo-keys/demoCA/client.pfx
must be imported as a user certificate by the browser to open Validation Console- Installation on Linux:
cd demo-keys; pk12util -d sql:$HOME/.pki/nssdb -i ./demoCA/client.pfx
- Installation on Linux:
- Notes:
Clean up certificates in demo-keys/demoCA/
. Each removal must be confirmed via rm -rvi
Called from npm run cache-bundle
to invoke cacheBundleUploadService.js
Called from npm run cache-bundle
to invoke cacheBundleGeneration.js
Called from npm run updateHtmlHash
to invoke cacheBundleGeneration.js
in loadOnly
mode
Install demo/node_modules
based on demo/package.json
for frontend modules for the demo. demo/package-lock.json
is updated.
Called from gulp demo-frontend-modules-locked
to install demo/node_modules
based on demo/package-lock.json
gulp.task('default',
gulp.series(
'build', // build hook.min.js
'build:test', // build test/hook.min.js
'examples', // hook examples/*
'demo' // build demo
)
);
gulp.task('examples',
gulp.series(
'script-examples', // hook non-module script examples
'module-examples', // hook module examples
'module-examples-dependencies' // save hook.parameters.moduleDependencies at examples/moduleDependencies.json
)
);
gulp.task('demo',
gulp.series(
'integrity-service-helpers', // build demo-backend/integrity-service-helpers/
'validation-console', // build demo-backend/validation-console/
'clean-gzip', // clean demo/*.gz
'get-version', // get version from the entry page demo/original-index.html
'demo-certificates', // generate certificates in demo-keys/demoCA/ if they are missing
'demo-keys', // generate key pairs and secret keys in demo-keys/keys.json
'import-maps', // generate import maps for demo at demo/modules.importmap
'browserify-commonjs', // build demo/browserify-commonjs.js
'webpack-es6-module', // build demo/webpack-es6-module.js
'webpack-commonjs', // build demo/webpack-commonjs.js
'rollup-es-modules', // build demo/rollup-module1.js and demo/rollup-es6-module.js
'policy', // configure demo/hook-callback.js
'disable-devtools', // configure demo/disable-devtools.js
'update-integrity-js', // update demo/integrity.js for the generated public keys in base64
'update-no-hook-authorization', // update demo/no-hook-authorization.js
'update-no-hook-authorization-in-html', // update hook.min.js?no-hook-authorization=* in HTMLs
'encode-demo-html', // generate demo/index.html from demo/original-index.html
'cache-bundle', // generate demo/cache-bundle.json via puppeteer
'integrity-json', // generate demo/integrity.json
'gzip', // gzip demo/cache-bundle.json and demo/integrity.json
'demo-frontend', // refresh and generate `demo-frontend/`
)
);
gulp.task('import-maps',
gulp.series(
'demo-frontend-modules-locked', // install demo/node_modules based on demo/package.json and demo/package-lock.json
'generate-import-maps', // generate import maps for demo frontend at demo/modules.importmap based on demo/node_modules/* and demo/modules-private.importmap
'embed-import-maps', // embed the generated import maps JSON into demo/bootstrap.js
)
);
gulp.task('cache-bundle',
gulp.series(
'get-version', // get version
'dummy-integrity', // generate dummy demo/integrity.json for build
'cache-bundle-automation-json', // generate dummy demo/cache-bundle.json for build
'cache-bundle-automation', // generate demo/cache-bundle.json via npm run cache-bundle
'script-hashes', // add script hashes to demo/cache-bundle.json
'script-hashes-integrity', // update integrity attributes of script-hashes.js script element in the entry page
'update-html-hash' // update "htmlHashHex" in demo-keys/keys.json via npm run updateHtmlHash
)
);
- Refine API
- Hook Coverage
- Hook Web Worker Scripts
- Hook Native APIs
- Consistent Contexts
- Track Asynchronous Calls
- Security Policies
- Framework for Access Control Policies
- Framework for Context Transition Policies
- Modularization of Policies
- Test Suites
- Demo
- Performance Optimization