-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create decoder for HTML entities #44
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,219 @@ | ||||||||||||||||||||||||||
package decoders | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||
"bytes" | ||||||||||||||||||||||||||
"errors" | ||||||||||||||||||||||||||
"regexp" | ||||||||||||||||||||||||||
"strconv" | ||||||||||||||||||||||||||
"strings" | ||||||||||||||||||||||||||
"sync" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
ahocorasick "github.com/BobuSumisu/aho-corasick" | ||||||||||||||||||||||||||
"github.com/go-logr/logr" | ||||||||||||||||||||||||||
"golang.org/x/exp/maps" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context" | ||||||||||||||||||||||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||||||||||||||||||||||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources" | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// HtmlEntity decodes characters that are encoded as decimal, hexadecimal, or named entities. | ||||||||||||||||||||||||||
// https://www.ee.ucl.ac.uk/~mflanaga/java/HTMLandASCIItableC1.html | ||||||||||||||||||||||||||
type HtmlEntity struct{} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
var ( | ||||||||||||||||||||||||||
_ Decoder = (*HtmlEntity)(nil) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
once sync.Once | ||||||||||||||||||||||||||
htmlTrie *ahocorasick.Trie | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func init() { | ||||||||||||||||||||||||||
// Use Aho-Corasick to pre-filter potential matches. | ||||||||||||||||||||||||||
once.Do(func() { | ||||||||||||||||||||||||||
keywords := map[string]struct{}{ | ||||||||||||||||||||||||||
`&#`: {}, // decimal | ||||||||||||||||||||||||||
`&#x`: {}, // hex | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
for entity := range namedEntityMap { | ||||||||||||||||||||||||||
keywords[strings.ToLower(entity)] = struct{}{} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
htmlTrie = ahocorasick.NewTrieBuilder().AddStrings(maps.Keys(keywords)).Build() | ||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func (d *HtmlEntity) Type() detectorspb.DecoderType { | ||||||||||||||||||||||||||
return detectorspb.DecoderType_HTML | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func (d *HtmlEntity) FromChunk(ctx context.Context, chunk *sources.Chunk) *DecodableChunk { | ||||||||||||||||||||||||||
if chunk == nil || len(chunk.Data) == 0 { | ||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||
} else if m := htmlTrie.MatchFirst(chunk.Data); m == nil { | ||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
var ( | ||||||||||||||||||||||||||
logger = ctx.Logger().WithName("decoders.html") | ||||||||||||||||||||||||||
// Necessary to avoid data races. | ||||||||||||||||||||||||||
chunkData = bytes.Clone(chunk.Data) | ||||||||||||||||||||||||||
matched = false | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
if namedEntityPat.Match(chunkData) { | ||||||||||||||||||||||||||
matched = true | ||||||||||||||||||||||||||
chunkData = decodeNamedEntities(logger, chunkData) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
if decimalEntityPat.Match(chunkData) { | ||||||||||||||||||||||||||
matched = true | ||||||||||||||||||||||||||
chunkData = decodeHtmlDecimal(logger, chunkData) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
if hexEntityPat.Match(chunkData) { | ||||||||||||||||||||||||||
matched = true | ||||||||||||||||||||||||||
chunkData = decodeHtmlHex(logger, chunkData) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if matched { | ||||||||||||||||||||||||||
return &DecodableChunk{ | ||||||||||||||||||||||||||
DecoderType: d.Type(), | ||||||||||||||||||||||||||
Chunk: &sources.Chunk{ | ||||||||||||||||||||||||||
Data: chunkData, | ||||||||||||||||||||||||||
SourceName: chunk.SourceName, | ||||||||||||||||||||||||||
SourceID: chunk.SourceID, | ||||||||||||||||||||||||||
JobID: chunk.JobID, | ||||||||||||||||||||||||||
SecretID: chunk.SecretID, | ||||||||||||||||||||||||||
SourceMetadata: chunk.SourceMetadata, | ||||||||||||||||||||||||||
SourceType: chunk.SourceType, | ||||||||||||||||||||||||||
Verify: chunk.Verify, | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// `A` = `A` | ||||||||||||||||||||||||||
var decimalEntityPat = regexp.MustCompile(`&#(\d{1,3});`) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func decodeHtmlDecimal(logger logr.Logger, input []byte) []byte { | ||||||||||||||||||||||||||
decoded := make([]byte, 0, len(input)) | ||||||||||||||||||||||||||
lastIndex := 0 | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
for _, match := range decimalEntityPat.FindAllSubmatchIndex(input, -1) { | ||||||||||||||||||||||||||
startIndex := match[0] | ||||||||||||||||||||||||||
endIndex := match[1] | ||||||||||||||||||||||||||
decStartIndex := match[2] | ||||||||||||||||||||||||||
decEndIndex := match[3] | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Copy the part of the input until the start of the entity | ||||||||||||||||||||||||||
decoded = append(decoded, input[lastIndex:startIndex]...) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
num, err := strconv.Atoi(string(input[decStartIndex:decEndIndex])) | ||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Append the decoded byte | ||||||||||||||||||||||||||
if num < 0 || num > 255 { | ||||||||||||||||||||||||||
logger.Error(errors.New("invalid decimal byte"), "Unable to decode HTML entity", "match", input[decStartIndex:decEndIndex], "byte", num) | ||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
decoded = append(decoded, byte(num)) | ||||||||||||||||||||||||||
lastIndex = endIndex | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Append the remaining part of the input | ||||||||||||||||||||||||||
decoded = append(decoded, input[lastIndex:]...) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
return decoded | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// `A` = `` | ||||||||||||||||||||||||||
var hexEntityPat = regexp.MustCompile(`(?i)&#x([a-f0-9]{1,2});`) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func decodeHtmlHex(logger logr.Logger, input []byte) []byte { | ||||||||||||||||||||||||||
decoded := make([]byte, 0, len(input)) | ||||||||||||||||||||||||||
lastIndex := 0 | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
for _, match := range hexEntityPat.FindAllSubmatchIndex(input, -1) { | ||||||||||||||||||||||||||
startIndex := match[0] | ||||||||||||||||||||||||||
endIndex := match[1] | ||||||||||||||||||||||||||
hexStartIndex := match[2] | ||||||||||||||||||||||||||
hexEndIndex := match[3] | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Copy the part of the input until the start of the entity | ||||||||||||||||||||||||||
decoded = append(decoded, input[lastIndex:startIndex]...) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Parse the hexadecimal value to an integer | ||||||||||||||||||||||||||
num, err := strconv.ParseInt(string(input[hexStartIndex:hexEndIndex]), 16, 32) | ||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Append the decoded byte | ||||||||||||||||||||||||||
if num < 0 || num > 255 { | ||||||||||||||||||||||||||
logger.Error(errors.New("invalid hex byte"), "Unable to decode HTML entity", "match", input[hexStartIndex:hexEndIndex], "byte", num) | ||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
decoded = append(decoded, byte(num)) | ||||||||||||||||||||||||||
Check failure Code scanning / CodeQL Incorrect conversion between integer types
Incorrect conversion of a signed 32-bit integer from [strconv.ParseInt](1) to a lower bit size type uint8 without an upper bound check.
Copilot Autofix AI about 1 month ago To fix the problem, we need to ensure that the parsed integer value is within the valid range for a
Suggested changeset
1
pkg/decoders/html_entity.go
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Refresh and try again.
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
lastIndex = endIndex | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Append the remaining part of the input | ||||||||||||||||||||||||||
decoded = append(decoded, input[lastIndex:]...) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
return decoded | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
var ( | ||||||||||||||||||||||||||
// https://www.compart.com/en/unicode/html | ||||||||||||||||||||||||||
namedEntityMap = map[string][]byte{ | ||||||||||||||||||||||||||
"&tab;": []byte(" "), | ||||||||||||||||||||||||||
"&newline;": []byte("\n"), | ||||||||||||||||||||||||||
"!": []byte("!"), | ||||||||||||||||||||||||||
""": []byte(`"`), | ||||||||||||||||||||||||||
"#": []byte("#"), | ||||||||||||||||||||||||||
"$": []byte("$"), | ||||||||||||||||||||||||||
"%": []byte("%"), | ||||||||||||||||||||||||||
"&": []byte("&"), | ||||||||||||||||||||||||||
"'": []byte("'"), | ||||||||||||||||||||||||||
"(": []byte("("), | ||||||||||||||||||||||||||
")": []byte(")"), | ||||||||||||||||||||||||||
"*": []byte("*"), | ||||||||||||||||||||||||||
"+": []byte("+"), | ||||||||||||||||||||||||||
",": []byte(","), | ||||||||||||||||||||||||||
".": []byte("."), | ||||||||||||||||||||||||||
"/": []byte("/"), | ||||||||||||||||||||||||||
":": []byte(":"), | ||||||||||||||||||||||||||
";": []byte(";"), | ||||||||||||||||||||||||||
"<": []byte("<"), | ||||||||||||||||||||||||||
"=": []byte("="), | ||||||||||||||||||||||||||
">": []byte(">"), | ||||||||||||||||||||||||||
"?": []byte("?"), | ||||||||||||||||||||||||||
"@": []byte("@"), | ||||||||||||||||||||||||||
"[": []byte("["), | ||||||||||||||||||||||||||
"\": []byte("\\"), | ||||||||||||||||||||||||||
"]": []byte("]"), | ||||||||||||||||||||||||||
"&hat;": []byte("^"), | ||||||||||||||||||||||||||
"&underbar;": []byte("_"), | ||||||||||||||||||||||||||
"&diacriticalgrave;": []byte("`"), | ||||||||||||||||||||||||||
"{": []byte("{"), | ||||||||||||||||||||||||||
"&verticalline;": []byte("|"), | ||||||||||||||||||||||||||
"}": []byte("}"), | ||||||||||||||||||||||||||
"&nonbreakingspace;": []byte(" "), | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
namedEntityPat = func() *regexp.Regexp { | ||||||||||||||||||||||||||
return regexp.MustCompile( | ||||||||||||||||||||||||||
"(?i)(" + strings.Join(maps.Keys(namedEntityMap), "|") + ")") | ||||||||||||||||||||||||||
}() | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
func decodeNamedEntities(_ logr.Logger, input []byte) []byte { | ||||||||||||||||||||||||||
return namedEntityPat.ReplaceAllFunc(input, func(match []byte) []byte { | ||||||||||||||||||||||||||
m := strings.ToLower(string(match)) | ||||||||||||||||||||||||||
if replacement, ok := namedEntityMap[m]; ok { | ||||||||||||||||||||||||||
return replacement | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
return match | ||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
} |
Check failure
Code scanning / CodeQL
Incorrect conversion between integer types
Copilot Autofix AI about 1 month ago
To fix the problem, we need to ensure that the integer value parsed from the string is within the valid range for a
byte
(0-255) before performing the conversion. This can be done by adding a bounds check after parsing the integer and before converting it to abyte
.strconv.Atoi
.byte
.