From 0135cfcb79c8d483ad13e34df9fcb86a6167d35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fabianski?= Date: Fri, 23 Feb 2024 14:04:47 +0100 Subject: [PATCH] feat: add timing attack js (#312) --- rules/go/gosec/file_permissions/file_perm.yml | 6 +- rules/go/gosec/file_permissions/mkdir.yml | 2 +- rules/go/lang/observable_timing.yml | 218 ++++++++++++++++++ rules/javascript/lang/file_permissions.yml | 6 +- rules/javascript/lang/observable_timing.yml | 80 +++++++ .../javascript/lang/observable_timing/test.js | 18 ++ .../lang/observable_timing/testdata/app.js | 33 +++ 7 files changed, 356 insertions(+), 7 deletions(-) create mode 100644 rules/go/lang/observable_timing.yml create mode 100644 rules/javascript/lang/observable_timing.yml create mode 100644 tests/javascript/lang/observable_timing/test.js create mode 100644 tests/javascript/lang/observable_timing/testdata/app.js diff --git a/rules/go/gosec/file_permissions/file_perm.yml b/rules/go/gosec/file_permissions/file_perm.yml index bd32ce6dc..7a658d3ec 100644 --- a/rules/go/gosec/file_permissions/file_perm.yml +++ b/rules/go/gosec/file_permissions/file_perm.yml @@ -17,11 +17,11 @@ auxiliary: filters: - either: - variable: MASK - regex: \A07 + regex: \A0o?7 - variable: MASK - regex: \A0\d[1-7] + regex: \A0o?\d[1-7] - variable: MASK - regex: \A0\d\d[1-7] + regex: \A0o?\d\d[1-7] languages: - go metadata: diff --git a/rules/go/gosec/file_permissions/mkdir.yml b/rules/go/gosec/file_permissions/mkdir.yml index 8dd8c3e8d..bd902c9eb 100644 --- a/rules/go/gosec/file_permissions/mkdir.yml +++ b/rules/go/gosec/file_permissions/mkdir.yml @@ -15,7 +15,7 @@ auxiliary: - pattern: $ filters: - variable: MASK - regex: \A077 + regex: \A0o?77 languages: - go metadata: diff --git a/rules/go/lang/observable_timing.yml b/rules/go/lang/observable_timing.yml new file mode 100644 index 000000000..bea3c1e1c --- /dev/null +++ b/rules/go/lang/observable_timing.yml @@ -0,0 +1,218 @@ +patterns: + - pattern: | + $ == $ + filters: + - variable: KEY1 + regex: /pass(word)?/ + - variable: KEY2 + regex: /pass(word)?/ + # - pattern: | + # return $X === auth_token; + # - pattern: | + # return auth_token === $X; + # - pattern: | + # return $X === token; + # - pattern: | + # return token === $X; + # - pattern: | + # return $X === hash; + # - pattern: | + # return hash === $X; + # - pattern: | + # return $X === password; + # - pattern: | + # return password === $X; + # - pattern: | + # return $X === pass; + # - pattern: | + # return pass === $X; + # - pattern: | + # return $X === apiKey; + # - pattern: | + # return apiKey === $X; + # - pattern: | + # return $X === apiSecret; + # - pattern: | + # return apiSecret === $X; + # - pattern: | + # return $X === api_key; + # - pattern: | + # return api_key === $X; + # - pattern: | + # return $X === api_secret; + # - pattern: | + # return api_secret === $X; + # - pattern: | + # return $X === secret; + # - pattern: | + # return secret === $X; + # - pattern: | + # return $X === api; + # - pattern: | + # return api === $X; + # - pattern: | + # return $X == auth_token; + # - pattern: | + # return auth_token == $X; + # - pattern: | + # return $X == token; + # - pattern: | + # return token == $X; + # - pattern: | + # return $X == hash; + # - pattern: | + # return hash == $X; + # - pattern: | + # return $X == password; + # - pattern: | + # return password == $X; + # - pattern: | + # return $X == pass; + # - pattern: | + # return pass == $X; + # - pattern: | + # return $X == apiKey; + # - pattern: | + # return apiKey == $X; + # - pattern: | + # return $X == apiSecret; + # - pattern: | + # return apiSecret == $X; + # - pattern: | + # return $X == api_key; + # - pattern: | + # return api_key == $X; + # - pattern: | + # return $X == api_secret; + # - pattern: | + # return api_secret == $X; + # - pattern: | + # return $X == secret; + # - pattern: | + # return secret == $X; + # - pattern: | + # return $X == api; + # - pattern: | + # return api == $X; + # - pattern: | + # return $X !== auth_token; + # - pattern: | + # return auth_token !== $X; + # - pattern: | + # return $X !== token; + # - pattern: | + # return token !== $X; + # - pattern: | + # return $X !== hash; + # - pattern: | + # return hash !== $X; + # - pattern: | + # return $X !== password; + # - pattern: | + # return password !== $X; + # - pattern: | + # return $X !== pass; + # - pattern: | + # return pass !== $X; + # - pattern: | + # return $X !== apiKey; + # - pattern: | + # return apiKey !== $X; + # - pattern: | + # return $X !== apiSecret; + # - pattern: | + # return apiSecret !== $X; + # - pattern: | + # return $X !== api_key; + # - pattern: | + # return api_key !== $X; + # - pattern: | + # return $X !== api_secret; + # - pattern: | + # return api_secret !== $X; + # - pattern: | + # return $X !== secret; + # - pattern: | + # return secret !== $X; + # - pattern: | + # return $X !== api; + # - pattern: | + # return api !== $X; + # - pattern: | + # return $X != auth_token; + # - pattern: | + # return auth_token != $X; + # - pattern: | + # return $X != token; + # - pattern: | + # return token != $X; + # - pattern: | + # return $X != hash; + # - pattern: | + # return hash != $X; + # - pattern: | + # return $X != password; + # - pattern: | + # return password != $X; + # - pattern: | + # return $X != pass; + # - pattern: | + # return pass != $X; + # - pattern: | + # return $X != apiKey; + # - pattern: | + # return apiKey != $X; + # - pattern: | + # return $X != apiSecret; + # - pattern: | + # return apiSecret != $X; + # - pattern: | + # return $X != api_key; + # - pattern: | + # return api_key != $X; + # - pattern: | + # return $X != api_secret; + # - pattern: | + # return api_secret != $X; + # - pattern: | + # return $X != secret; + # - pattern: | + # return secret != $X; + # - pattern: | + # return $X != api; + # - pattern: | + # return api != $X; + +# auxiliary: +# - id: go_lang_observable_timing_init +# patterns: +# - pattern1 +# - pattern: $ +# filters: +# - variable: INIT +# detection: go_lang_observable_timing_instance +# scope: cursor +# - id: go_lang_observable_timing_instance +# patterns: +# - pattern2 +languages: + - go +metadata: + description: "" + remediation_message: | + ## Description + + ## Remediations + + ✅ + + ❌ + + ## References + + - []() + + cwe_id: + - 208 + id: go_lang_observable_timing + documentation_url: https://docs.bearer.com/reference/rules/go_lang_observable_timing diff --git a/rules/javascript/lang/file_permissions.yml b/rules/javascript/lang/file_permissions.yml index f07c1ad9d..ca1a2a203 100644 --- a/rules/javascript/lang/file_permissions.yml +++ b/rules/javascript/lang/file_permissions.yml @@ -31,11 +31,11 @@ auxiliary: filters: - either: - variable: MASK - regex: \A0o7 + regex: \A0o?7 - variable: MASK - regex: \A0o\d[1-7] + regex: \A0o?\d[1-7] - variable: MASK - regex: \A0o\d\d[1-7] + regex: \A0o?\d\d[1-7] languages: - javascript severity: high diff --git a/rules/javascript/lang/observable_timing.yml b/rules/javascript/lang/observable_timing.yml new file mode 100644 index 000000000..049338a56 --- /dev/null +++ b/rules/javascript/lang/observable_timing.yml @@ -0,0 +1,80 @@ +patterns: + - pattern: | + $ == $ + filters: + - variable: KEY1 + detection: javascript_lang_observable_timing_regex + scope: cursor + - not: + variable: OTHER + detection: javascript_lang_observable_timing_other + scope: cursor + + - pattern: | + $ == $ + filters: + - variable: KEY1 + detection: javascript_lang_observable_timing_regex + scope: cursor + - not: + variable: OTHER + detection: javascript_lang_observable_timing_other + scope: cursor + +auxiliary: + - id: javascript_lang_observable_timing_regex + patterns: + - pattern: $ + filters: + - variable: REGEX + regex: (?i)\A(password)|hash|(api|auth)?.?(token|secret)\z + + - id: javascript_lang_observable_timing_other + patterns: + - pattern: $ + filters: + - variable: OTHER + regex: ^((null)|(undefined)) + +languages: + - javascript +metadata: + description: "Observable Timing Discrepancy" + remediation_message: | + ## Description + + Observable Timing Discrepancy refers to vulnerabilities arising from attackers being able to observe differences in the time it takes to perform certain operations. + These discrepancies can lead to the exposure of sensitive information, as attackers can use timing analysis to infer secrets based on the execution time of algorithms, particularly during comparisons of user input against secret values. + + ## Remediations + + ✅ **Implement Constant Time Algorithms** + + Ensure algorithms that process sensitive information, such as password comparisons, execute in constant time to prevent timing attacks. + + ✅ **Use Built-in Security Features** + + Leverage built-in cryptographic libraries that include timing-attack safe functions for comparing secrets. + + ✅ **Minimize Client-Side Checks** + + Avoid sensitive comparisons directly in client-side JavaScript where possible, as the execution environment is more exposed to timing analysis by attackers. + + ❌ **Avoid Direct Comparisons** + + Do not use direct string comparisons for sensitive information that can lead to early function termination based on the first incorrect character. + + ❌ **Do Not Rely on Execution Time for Logic** + + Ensure the application's logic does not change execution paths in a way that could introduce observable timing differences based on user input or secret values. + + ## Resources + + - [CWE-208: Observable Timing Discrepancy](https://cwe.mitre.org/data/definitions/208.html) + - [OWASP Guide to Cryptography](https://owasp.org/www-project-cheat-sheets/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) + - [MDN Web Docs on SubtleCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) + + cwe_id: + - 409 + id: javascript_lang_observable_timing + documentation_url: https://docs.bearer.com/reference/rules/javascript_lang_observable_timing diff --git a/tests/javascript/lang/observable_timing/test.js b/tests/javascript/lang/observable_timing/test.js new file mode 100644 index 000000000..a350cb108 --- /dev/null +++ b/tests/javascript/lang/observable_timing/test.js @@ -0,0 +1,18 @@ +const { + createNewInvoker, + getEnvironment, +} = require("../../../helper.js") +const { ruleId, ruleFile, testBase } = getEnvironment(__dirname) + +describe(ruleId, () => { + const invoke = createNewInvoker(ruleId, ruleFile, testBase) + + test("observable_timing", () => { + const testCase = "app.js" + + const results = invoke(testCase) + + expect(results.Missing).toEqual([]) + expect(results.Extra).toEqual([]) + }) +}) \ No newline at end of file diff --git a/tests/javascript/lang/observable_timing/testdata/app.js b/tests/javascript/lang/observable_timing/testdata/app.js new file mode 100644 index 000000000..51b8d09e5 --- /dev/null +++ b/tests/javascript/lang/observable_timing/testdata/app.js @@ -0,0 +1,33 @@ +// Use bearer:expected javascript_lang_observable_timing to flag expected findings + +// bearer:expected javascript_lang_observable_timing +password == "zDE9ET!TDq2uZx2oM!FD2" +// bearer:expected javascript_lang_observable_timing +"zDE9ET!TDq2uZx2oM!FD2" == password +// bearer:expected javascript_lang_observable_timing +"zDE9ET!TDq2uZx2oM!FD2" === password +// bearer:expected javascript_lang_observable_timing +password == passwordInput +// bearer:expected javascript_lang_observable_timing +hash === verify.toString("base64") + +function isAuthenticated(username, input) { + var user = FetchUserFromDB(username) + if (input.length !== user.token.length) { + return false + } + + // bearer:expected javascript_lang_observable_timing + return input === user.token +} + +if (password !== null) { +} + +// bearer:expected javascript_lang_observable_timing +if (apiToken === "zDE9ET!TDq2uZx2oM!FD2") { +} + +if (inputToken.length !== secretToken.length) { + return false +}